Subject: [PATCH v2] services: elogind: Split sleep.conf and port to define-configuration.

From: Maxim Cournoyer <[email-masked]>
Date: Fri, 25 Apr 2025 11:10:16 +0900

* gnu/services/desktop.scm (pascal-case): New procedure.
(<elogind-configuration>): Rewrite in terms of define-configuration.
(elogind-configuration-file): Delete.
(maybe-list-of-suspend-states?, maybe-list-of-suspend-modes?)
maybe-list-of-user-names?, maybe-boolean?maybe-package?)
(maybe-action?, maybe-percent?, maybe-list-of-strings?)
(maybe-list-of-hibernation-modes?, maybe-non-negative-integer?)
(non-negative-integer?, percent?, char-set:user-name, user-name?)
(list-of-user-names?, %elogind-actions, action?, %linux-suspend-states)
(string->symbol/maybe, suspend-state?, list-of-suspend-states?)
(%linux-suspend-modes, suspend-mode?, list-of-suspend-modes?)
(%linux-hibernation-modes, hibernation-mode?, list-of-hibernation-modes?)
(elogind-deprecated-empty-serializer, list-of-file-likes?)
(elogind-serialize-boolean, elogind-base-serializer, elogind-serialize-action)
(elogind-serialize-non-negative-integer, elogind-serialize-percent)
(elogind-list-serializer, elogind-serialize-list-of-strings)
(elogind-serialize-list-of-user-names, elogind-serialize-list-of-suspend-states)
(elogind-serialize-list-of-suspend-modes)
(elogind-serialize-list-of-hibernation-modes)
(%elogind-configuration-sleep-fields, logind.conf, sleep.conf): New procedures.
(elogind-etc-directory): Create the main configuration files there too.
(elogind-dbus-service): Adjust for package accessor name change.
(pam-extension-procedure, elogind-shepherd-service)
(elogind-service-type):  Likewise.
* doc/guix.texi (Desktop Services): Fully document configuration options.

Fixes: bug#77806
Change-Id: I8767891871d83e58d64995ec986a7d01689fa6d8
---
 doc/guix.texi            | 191 +++++++-----
 gnu/services/desktop.scm | 628 ++++++++++++++++++++++++++-------------
 2 files changed, 524 insertions(+), 295 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index be2fbbaf5bc..7b418a40892 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -26264,129 +26264,158 @@ Desktop Services
 @code{<elogind-configuration>} object.
 @end defvar
 
-@c TODO: field descriptions. This is best done by refactoring
-@c elogind-configuration to use define-configuration which embeds the
-@c descriptions in the code and then use configuration->documentation.
+@c Auto-generated via (configuration->documentation 'elogind-configuration).
+@c %start of fragment
+
 @deftp {Data Type} elogind-configuration
-Data type representing the configuration of @command{elogind}.
+Available @code{elogind-configuration} fields are:
 
 @table @asis
 @item @code{elogind} (default: @code{elogind}) (type: file-like)
-...
+The elogind package to use.
 
-@item @code{kill-user-processes?} (default: @code{#f}) (type: boolean)
-...
+@item @code{system-sleep-hook-files} (default: @code{()}) (type: list-of-file-likes)
+A list of executables (file-like objects) that will be installed into
+the @file{/etc/elogind/system-sleep} hook directory.  See `Hook
+directories' in the @samp{loginctl(1)} man page for more information.
 
-@item @code{kill-only-users} (default: @code{'()}) (type: list)
-...
+@item @code{system-shutdown-hook-files} (default: @code{()}) (type: list-of-file-likes)
+A list of executables (file-like objects) that will be installed into
+the @file{/etc/elogind/system-shutdown/} hook directory.
 
-@item @code{kill-exclude-users} (default: @code{'("root")}) (type: list-of-string)
-...
+@item @code{allow-power-off-interrupts?} (default: @code{#f}) (type: maybe-boolean)
+Whether the executables in elogind's hook directories (see above) can
+cause a power-off action to be cancelled (interrupted) by printing an
+appropriate error message to stdout.
 
-@item @code{inhibit-delay-max-seconds} (default: @code{5}) (type: integer)
-...
+@item @code{allow-suspend-interrupts?} (default: @code{#f}) (type: maybe-boolean)
+Likewise as the @code{allow-power-off-interrupts?} option, but for the
+suspend action.
 
-@item @code{handle-power-key} (default: @code{'poweroff}) (type: symbol)
-...
+@item @code{broadcast-power-off-interrupts?} (default: @code{#f}) (type: maybe-boolean)
+Whether an interrupt of a power-off action is broadcasted.
 
-@item @code{handle-suspend-key} (default: @code{'suspend}) (type: symbol)
-...
+@item @code{broadcast-suspend-interrupts?} (default: @code{#f}) (type: maybe-boolean)
+Whether an interrupt of a suspend action is broadcasted.
 
-@item @code{handle-hibernate-key} (default: @code{'hibernate}) (type: symbol)
-...
+@item @code{kill-user-processes?} (default: @code{#f}) (type: maybe-boolean)
+Whether the processes of a user should be killed when the user logs out.
 
-@item @code{handle-lid-switch} (default: @code{'suspend}) (type: symbol)
-...
+@item @code{kill-only-users} (type: maybe-list-of-user-names)
+Usernames whose processes should be killed, regardless the value of
+@code{kill-user-processes?}.
 
-@item @code{handle-lid-switch-docked} (default: @code{'ignore}) (type: symbol)
-...
+@item @code{kill-exclude-users} (default: @code{("root")}) (type: maybe-list-of-user-names)
+Usernames whose processes should @emph{not} be killed, regardless the
+value of @code{kill-user-processes?}.
 
-@item @code{handle-lid-switch-external-power} (default: @code{*unspecified*}) (type: symbol)
-...
+@item @code{inhibit-delay-max-seconds} (default: @code{5}) (type: maybe-non-negative-integer)
+The maximum time a system shutdown or sleep request is delayed due to an
+inhibitor lock of type delay being active before the inhibitor is
+ignored and the operation executes anyway.
 
-@item @code{power-key-ignore-inhibited?} (default: @code{#f}) (type: boolean)
-...
+@item @code{handle-power-key} (default: @code{poweroff}) (type: maybe-action)
+The action done when the power key is pressed.  The compiled default is
+@code{'poweroff}.
 
-@item @code{suspend-key-ignore-inhibited?} (default: @code{#f}) (type: boolean)
-...
+@item @code{handle-suspend-key} (default: @code{suspend}) (type: maybe-action)
+The action done when the suspend key is pressed.  The
 
-@item @code{hibernate-key-ignore-inhibited?} (default: @code{#f}) (type: boolean)
-...
+@item @code{handle-hibernate-key} (default: @code{hibernate}) (type: maybe-action)
+The action done when the hibernate key is pressed.
 
-@item @code{lid-switch-ignore-inhibited?} (default: @code{#t}) (type: boolean)
-...
+@item @code{handle-lid-switch} (default: @code{suspend}) (type: maybe-action)
+The action done when the lid is closed.
 
-@item @code{holdoff-timeout-seconds} (default: @code{30}) (type: integer)
-...
+@item @code{handle-lid-switch-docked} (default: @code{ignore}) (type: maybe-action)
+The action done when the lid is closed and the device docked.
 
-@item @code{idle-action} (default: @code{'ignore}) (type: symbol)
-...
+@item @code{handle-lid-switch-external-power} (default: @code{suspend}) (type: maybe-action)
+The action done when the lid is closed and the device is externally
+powered.
 
-@item @code{idle-action-seconds} (default: @code{(* 30 60)}) (type: integer)
-...
+@item @code{power-key-ignore-inhibited?} (default: @code{#f}) (type: maybe-boolean)
+Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+idle) when the power key is pressed.
 
-@item @code{runtime-directory-size-percent} (default: @code{10}) (type: integer)
-...
+@item @code{suspend-key-ignore-inhibited?} (default: @code{#f}) (type: maybe-boolean)
+Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+idle) when the suspend key is pressed.
 
-@item @code{runtime-directory-size} (default: @code{#f}) (type: integer)
-...
+@item @code{hibernate-key-ignore-inhibited?} (default: @code{#f}) (type: maybe-boolean)
+Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+idle) when the hibernate key is pressed.
 
-@item @code{remove-ipc?} (default: @code{#t}) (type: boolean)
-...
+@item @code{lid-switch-ignore-inhibited?} (default: @code{#f}) (type: maybe-boolean)
+Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+idle) when the lid is closed.
 
-@item @code{suspend-state} (default: @code{'("mem" "standby" "freeze")}) (type: list)
-...
+@item @code{holdoff-timeout-seconds} (default: @code{30}) (type: maybe-non-negative-integer)
+Specifies the number of seconds after system startup or system resume
+during which elogind will hold off on reacting to lid events.
 
-@item @code{suspend-mode} (default: @code{'()}) (type: list)
-...
+@item @code{idle-action} (default: @code{ignore}) (type: maybe-action)
+Action to take when the system is idle.
 
-@item @code{hibernate-state} (default: @code{'("disk")}) (type: list)
-...
+@item @code{idle-action-seconds} (type: maybe-non-negative-integer)
+The delay after which the action configured in @code{idle-action} is
+taken after the system is idle.
 
-@item @code{hibernate-mode} (default: @code{'("platform" "shutdown")}) (type: list)
-...
+@item @code{runtime-directory-size-percent} (type: maybe-percent)
+Set the size limit, in percent, on the @env{XDG_RUNTIME_DIR} runtime
+directory for each user who logs in.  This specifies the per-user size
+limit relative to the amount of physical @acronym{RAM,read access
+memory}.  This value takes precedence over that specified via
+@code{runtime-directory-size}.
 
-@item @code{hybrid-sleep-state} (default: @code{'("disk")}) (type: list)
-...
+@item @code{runtime-directory-size} (type: maybe-non-negative-integer)
+Set the size limit, in bytes, on the @env{XDG_RUNTIME_DIR} runtime
+directory for each user who logs in.
 
-@item @code{hybrid-sleep-mode} (default: @code{'("suspend" "platform" "shutdown")}) (type: list)
-...
+@item @code{remove-ipc?} (default: @code{#t}) (type: maybe-boolean)
+Whether @acronym{IPC,inter-process communication} objects belonging to
+the user shall be removed when the user fully logs out.
 
-@item @code{hibernate-delay-seconds} (default: @code{*unspecified*}) (type: integer)
-...
+@item @code{suspend-state} (default: @code{(mem standby freeze)}) (type: maybe-list-of-suspend-states)
+The suspend state values to be write to @file{/sys/power/state} by
+elogind when suspending the system.  They will be tried in turn, until
+one is written without error.
 
-@item @code{suspend-estimation-seconds} (default: @code{*unspecified*}) (type: integer)
-...
+@item @code{suspend-mode} (type: maybe-list-of-suspend-modes)
+The suspend mode values to write to @file{/sys/power/mem_sleep} by
+elogind when suspending the system.
 
-@item @code{system-sleep-hook-files} (default: @code{'()}) (type: list)
-A list of executables (file-like objects) that will be installed into
-the @file{/etc/elogind/system-sleep/} hook directory. For example:
+@item @code{suspend-estimation-seconds} (default: @code{3600}) (type: maybe-non-negative-integer)
+Cause the RTC alarm to wake the system after the specified time span to
+measure the system battery capacity level and estimate the battery
+discharging rate, which is used for estimating the time span until the
+system battery charge level goes down to 5%.  This option is only used
+by elogind when using the @code{'suspend-then-hibernate} action.
 
-@lisp
-(elogind-configuration
- (system-sleep-hook-files
-  (list (local-file "sleep-script"))))
-@end lisp
+@item @code{hibernate-mode} (default: @code{(platform shutdown)}) (type: maybe-list-of-hibernation-modes)
+The hibernation mode values to write to @file{/sys/power/disk} by
+elogind when hibernating the system.
 
-See `Hook directories' in the @code{loginctl(1)} man page for more information.
+@item @code{hibernate-delay-seconds} (type: maybe-non-negative-integer)
+The amount of time the system spends in suspend mode before the system
+is automatically put into hibernate mode.
 
-@item @code{system-shutdown-hook-files} (default: @code{'()}) (type: list)
-A list of executables (file-like objects) that will be installed into
-the @file{/etc/elogind/system-shutdown/} hook directory.
+@item @code{hibernate-state} (type: maybe-list-of-strings)
+Deprecated option.
 
-@item @code{allow-power-off-interrupts?} (default: @code{#f}) (type: boolean)
-@itemx @code{allow-suspend-interrupts?} (default: @code{#f}) (type: boolean)
-Whether the executables in elogind's hook directories (see above) can
-cause a power-off or suspend action to be cancelled (interrupted) by
-printing an appropriate error message to stdout.
+@item @code{hybrid-sleep-state} (type: maybe-list-of-strings)
+Deprecated option.
 
-@item @code{broadcast-power-off-interrupts?} (default: @code{#t}) (type: boolean)
-@itemx @code{broadcast-suspend-interrupts?} (default: @code{#t}) (type: boolean)
-Whether an interrupt of a power-off or suspend action is broadcasted.
+@item @code{hybrid-sleep-mode} (type: maybe-list-of-strings)
+Deprecated option.
 
 @end table
+
 @end deftp
 
+
+@c %end of fragment
+
 @defvar accountsservice-service-type
 Type for the service that runs AccountsService, a system service that can
 list available accounts, change their passwords, and so on.
diff --git a/gnu/services/desktop.scm b/gnu/services/desktop.scm
index fe034cfa8f4..e422fbc89df 100644
--- a/gnu/services/desktop.scm
+++ b/gnu/services/desktop.scm
@@ -37,6 +37,7 @@
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (gnu services desktop)
+  #:use-module ((gnu home services utils) #:select (object->camel-case-string))
   #:use-module (gnu services)
   #:use-module (gnu services shepherd)
   #:use-module (gnu services base)
@@ -228,6 +229,8 @@ (define (package-direct-input-selector tree)
           (loop (cdr tree)
                 (car (assoc-ref (package-direct-inputs package)
                                 (car tree))))))))
+(define (pascal-case text)
+  (object->camel-case-string text 'upper))
 
 
 ;;;
@@ -1025,173 +1028,367 @@ (define gvfs-service-type
 ;;; Elogind login and seat management service.
 ;;;
 
-(define-record-type* <elogind-configuration> elogind-configuration
-  make-elogind-configuration
-  elogind-configuration?
-  (elogind                          elogind-package
-                                    (default elogind))
-  (kill-user-processes?             elogind-kill-user-processes?
-                                    (default #f))
-  (kill-only-users                  elogind-kill-only-users
-                                    (default '()))
-  (kill-exclude-users               elogind-kill-exclude-users
-                                    (default '("root")))
-  (inhibit-delay-max-seconds        elogind-inhibit-delay-max-seconds
-                                    (default 5))
-  (handle-power-key                 elogind-handle-power-key
-                                    (default 'poweroff))
-  (handle-suspend-key               elogind-handle-suspend-key
-                                    (default 'suspend))
-  (handle-hibernate-key             elogind-handle-hibernate-key
-                                    (default 'hibernate))
-  (handle-lid-switch                elogind-handle-lid-switch
-                                    (default 'suspend))
-  (handle-lid-switch-docked         elogind-handle-lid-switch-docked
-                                    (default 'ignore))
-  (handle-lid-switch-external-power elogind-handle-lid-switch-external-power
-                                    (default *unspecified*))
-  (power-key-ignore-inhibited?      elogind-power-key-ignore-inhibited?
-                                    (default #f))
-  (suspend-key-ignore-inhibited?    elogind-suspend-key-ignore-inhibited?
-                                    (default #f))
-  (hibernate-key-ignore-inhibited?  elogind-hibernate-key-ignore-inhibited?
-                                    (default #f))
-  (lid-switch-ignore-inhibited?     elogind-lid-switch-ignore-inhibited?
-                                    (default #t))
-  (holdoff-timeout-seconds          elogind-holdoff-timeout-seconds
-                                    (default 30))
-  (idle-action                      elogind-idle-action
-                                    (default 'ignore))
-  (idle-action-seconds              elogind-idle-action-seconds
-                                    (default (* 30 60)))
-  (runtime-directory-size-percent   elogind-runtime-directory-size-percent
-                                    (default 10))
-  (runtime-directory-size           elogind-runtime-directory-size
-                                    (default #f))
-  (remove-ipc?                      elogind-remove-ipc?
-                                    (default #t))
-
-  (suspend-state                    elogind-suspend-state
-                                    (default '("mem" "standby" "freeze")))
-  (suspend-mode                     elogind-suspend-mode
-                                    (default '()))
-  (hibernate-state                  elogind-hibernate-state
-                                    (default '("disk")))
-  (hibernate-mode                   elogind-hibernate-mode
-                                    (default '("platform" "shutdown")))
-  (hybrid-sleep-state               elogind-hybrid-sleep-state
-                                    (default '("disk")))
-  (hybrid-sleep-mode                elogind-hybrid-sleep-mode
-                                    (default
-                                      '("suspend" "platform" "shutdown")))
-  (hibernate-delay-seconds          elogind-hibernate-delay-seconds
-                                    (default *unspecified*))
-  (suspend-estimation-seconds       elogind-suspend-estimation-seconds
-                                    (default *unspecified*))
-  (system-sleep-hook-files          elogind-system-sleep-hook-files
-                                    (default '()))
-  (system-shutdown-hook-files       elogind-system-shutdown-hook-files
-                                    (default '()))
-  (allow-power-off-interrupts?      elogind-allow-power-off-interrupts?
-                                    (default #f))
-  (allow-suspend-interrupts?        elogind-allow-suspend-interrupts?
-                                    (default #f))
-  (broadcast-power-off-interrupts?  elogind-broadcast-power-off-interrupts?
-                                    (default #t))
-  (broadcast-suspend-interrupts?    elogind-broadcast-suspend-interrupts?
-                                    (default #t)))
-
-(define (elogind-configuration-file config)
-  (define (yesno x)
-    (match x
-      (#t "yes")
-      (#f "no")
-      (_ (error "expected #t or #f, instead got:" x))))
-  (define char-set:user-name
-    (string->char-set "abcdefghijklmnopqrstuvwxyz0123456789_-"))
-  (define (valid-list? l pred)
-    (and-map (lambda (x) (string-every pred x)) l))
-  (define (user-name-list users)
-    (unless (valid-list? users char-set:user-name)
-      (error "invalid user list" users))
-    (string-join users " "))
-  (define (enum val allowed)
-    (unless (memq val allowed)
-      (error "invalid value" val allowed))
-    (symbol->string val))
-  (define (non-negative-integer x)
-    (unless (exact-integer? x) (error "not an integer" x))
-    (when (negative? x) (error "negative number not allowed" x))
-    (number->string x))
-  (define (maybe-non-negative-integer x)
-    (or (and (unspecified? x) x)
-        (non-negative-integer x)))
-  (define handle-actions
-    '(ignore poweroff reboot halt kexec suspend hibernate hybrid-sleep suspend-then-hibernate lock))
-  (define (handle-action x)
-    (if (unspecified? x)
-        x                               ;let the unspecified value go through
-        (enum x handle-actions)))
-  (define (sleep-list tokens)
-    (unless (valid-list? tokens char-set:user-name)
-      (error "invalid sleep list" tokens))
-    (string-join tokens " "))
-  (define-syntax ini-file-clause
-    (syntax-rules ()
-      ;; Produce an empty line when encountering an unspecified value.  This
-      ;; is better than an empty string value, which can, in some cases, cause
-      ;; warnings such as "Failed to parse handle action setting".
-      ((_ config (prop (parser getter)))
-       (let ((value (parser (getter config))))
-         (if (unspecified? value)
-             ""
-             (string-append prop "=" value "\n"))))
-      ((_ config str)
-       (if (unspecified? str)
-           ""
-           (string-append str "\n")))))
-  (define-syntax-rule (ini-file config file clause ...)
-    (plain-file file (string-append (ini-file-clause config clause) ...)))
-  (ini-file
-   config "logind.conf"
-   "[Login]"
-   ("KillUserProcesses" (yesno elogind-kill-user-processes?))
-   ("KillOnlyUsers" (user-name-list elogind-kill-only-users))
-   ("KillExcludeUsers" (user-name-list elogind-kill-exclude-users))
-   ("InhibitDelayMaxSec" (non-negative-integer elogind-inhibit-delay-max-seconds))
-   ("HandlePowerKey" (handle-action elogind-handle-power-key))
-   ("HandleSuspendKey" (handle-action elogind-handle-suspend-key))
-   ("HandleHibernateKey" (handle-action elogind-handle-hibernate-key))
-   ("HandleLidSwitch" (handle-action elogind-handle-lid-switch))
-   ("HandleLidSwitchDocked" (handle-action elogind-handle-lid-switch-docked))
-   ("HandleLidSwitchExternalPower" (handle-action elogind-handle-lid-switch-external-power))
-   ("PowerKeyIgnoreInhibited" (yesno elogind-power-key-ignore-inhibited?))
-   ("SuspendKeyIgnoreInhibited" (yesno elogind-suspend-key-ignore-inhibited?))
-   ("HibernateKeyIgnoreInhibited" (yesno elogind-hibernate-key-ignore-inhibited?))
-   ("LidSwitchIgnoreInhibited" (yesno elogind-lid-switch-ignore-inhibited?))
-   ("HoldoffTimeoutSec" (non-negative-integer elogind-holdoff-timeout-seconds))
-   ("IdleAction" (handle-action elogind-idle-action))
-   ("IdleActionSec" (non-negative-integer elogind-idle-action-seconds))
-   ("RuntimeDirectorySize"
-    (identity
-     (lambda (config)
-       (match (elogind-runtime-directory-size-percent config)
-         (#f (non-negative-integer (elogind-runtime-directory-size config)))
-         (percent (string-append (non-negative-integer percent) "%"))))))
-   ("RemoveIPC" (yesno elogind-remove-ipc?))
-   "[Sleep]"
-   ("SuspendState" (sleep-list elogind-suspend-state))
-   ("SuspendMode" (sleep-list elogind-suspend-mode))
-   ("HibernateState" (sleep-list elogind-hibernate-state))
-   ("HibernateMode" (sleep-list elogind-hibernate-mode))
-   ("HybridSleepState" (sleep-list elogind-hybrid-sleep-state))
-   ("HybridSleepMode" (sleep-list elogind-hybrid-sleep-mode))
-   ("HibernateDelaySec" (maybe-non-negative-integer elogind-hibernate-delay-seconds))
-   ("SuspendEstimationSec" (maybe-non-negative-integer elogind-suspend-estimation-seconds))
-   ("AllowPowerOffInterrupts" (yesno elogind-allow-power-off-interrupts?))
-   ("AllowSuspendInterrupts" (yesno elogind-allow-suspend-interrupts?))
-   ("BroadcastPowerOffInterrupts" (yesno elogind-broadcast-power-off-interrupts?))
-   ("BroadcastSuspendInterrupts" (yesno elogind-broadcast-suspend-interrupts?))))
+;;; Elogind configuration types.
+(define-maybe boolean
+  (prefix elogind-))
+
+(define (non-negative-integer? x)
+  (and (exact-integer? x)
+       (not (negative? x))))
+
+(define-maybe non-negative-integer
+  (prefix elogind-))
+
+(define (percent? x)
+  (and (non-negative-integer? x)
+       (>= x 0)
+       (<= x 100)))
+
+(define-maybe percent
+  (prefix elogind-))
+
+(define char-set:user-name
+  (string->char-set "abcdefghijklmnopqrstuvwxyz0123456789_-"))
+
+(define (user-name? x)
+  (string-every char-set:user-name x))
+
+(define list-of-user-names?
+  (list-of user-name?))
+
+(define-maybe list-of-user-names
+  (prefix elogind-))
+
+(define %elogind-actions
+  '( ignore poweroff reboot halt kexec suspend hibernate hybrid-sleep
+     suspend-then-hibernate lock factory-reset))
+
+(define (action? x)
+  (member x %elogind-actions))
+
+(define-maybe action
+  (prefix elogind-))
+
+(define %linux-suspend-states
+  ;; The possible suspend states supported by the Linux kernel.
+  ;; See (info "(linux) Basic sysfs Interfaces for System Suspend and Hibernation").
+  '(disk standby freeze mem))
+
+(define (string->symbol/maybe x)
+  (if (string? x)
+      (string->symbol x)
+      x))
+
+(define (suspend-state? x)
+  (member (string->symbol/maybe x) %linux-suspend-states))
+
+(define list-of-suspend-states?
+  (list-of suspend-state?))
+
+(define-maybe list-of-suspend-states
+  (prefix elogind-))
+
+(define %linux-suspend-modes
+  ;; The possible suspend state variants supported by the Linux kernel.
+  ;; See (info "(linux) Basic sysfs Interfaces for System Suspend and Hibernation").
+  '(s2idle shallow deep))
+
+(define (suspend-mode? x)
+  (member (string->symbol/maybe x) %linux-suspend-modes))
+
+(define list-of-suspend-modes?
+  (list-of suspend-mode?))
+
+(define-maybe list-of-suspend-modes
+  (prefix elogind-))
+
+(define %linux-hibernation-modes
+  ;; The possible hibernation operating modes supported by the Linux kernel.
+  ;; See (info "(linux) Basic sysfs Interfaces for System Suspend and Hibernation").
+  '(platform shutdown reboot suspend test_resume))
+
+(define (hibernation-mode? x)
+  (member (string->symbol/maybe x) %linux-hibernation-modes))
+
+(define list-of-hibernation-modes?
+  (list-of hibernation-mode?))
+
+(define-maybe list-of-hibernation-modes
+  (prefix elogind-))
+
+(define (elogind-deprecated-empty-serializer name value)
+  (when (maybe-value-set? value)
+    (warn-about-deprecation name #f
+                            #:replacement #f))
+  "")
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define-maybe list-of-strings
+  (prefix elogind-))
+
+;;; Elogind serializers.
+(define (elogind-serialize-boolean name value)
+  (let* ((name-str (symbol->string name))
+         (name (if (string-suffix? "?" name-str)
+                   (string-drop-right name-str 1)
+                   name-str)))
+    (format #f "~a=~:[no~;yes~]~%" (pascal-case name) value)))
+
+(define (elogind-base-serializer name value)
+  (let* ((name-str (symbol->string name))
+         (name (if (string-suffix? "seconds" name-str)
+                   (string-drop-right name-str 4) ;seconds -> sec
+                   name-str)))
+    (format #f "~a=~a~%" (pascal-case name) value)))
+
+(define elogind-serialize-action elogind-base-serializer)
+(define elogind-serialize-non-negative-integer elogind-base-serializer)
+(define elogind-serialize-percent elogind-base-serializer)
+
+(define (elogind-list-serializer name value)
+  (format #f "~a=~{~a~^ ~}~%" (pascal-case name) value))
+
+(define elogind-serialize-list-of-strings elogind-list-serializer)
+(define elogind-serialize-list-of-user-names elogind-list-serializer)
+(define elogind-serialize-list-of-suspend-states elogind-list-serializer)
+(define elogind-serialize-list-of-suspend-modes elogind-list-serializer)
+(define elogind-serialize-list-of-hibernation-modes elogind-list-serializer)
+
+;;; XXX: For backward-compatible/historical reasons, the configuration object
+;;; is flat, containing the fields of both the logind.conf and sleep.conf
+;;; files.  The list below contains the fields that should be serialized to
+;;; sleep.conf.
+(define %elogind-configuration-sleep-fields
+  '( suspend-state suspend-mode suspend-estimation-seconds
+     hibernate-mode hibernate-delay-seconds hibernate-state
+     hybrid-sleep-state hybrid-sleep-mode))
+
+(define-configuration elogind-configuration
+  (elogind
+   (file-like elogind)
+   "The elogind package to use."
+   (serializer empty-serializer))
+
+  (system-sleep-hook-files
+   (list-of-file-likes '())
+   "A list of executables (file-like objects) that will be installed into the
+@file{/etc/elogind/system-sleep} hook directory.  See `Hook directories' in
+the @samp{loginctl(1)} man page for more information."
+   (serializer empty-serializer))
+
+  (system-shutdown-hook-files
+   (list-of-file-likes '())
+   "A list of executables (file-like objects) that will be installed into the
+@file{/etc/elogind/system-shutdown/} hook directory."
+   (serializer empty-serializer))
+
+  (allow-power-off-interrupts?
+   (maybe-boolean #f)
+   "Whether the executables in elogind's hook directories (see above) can
+  cause a power-off action to be cancelled (interrupted) by printing an
+  appropriate error message to stdout.")
+
+  (allow-suspend-interrupts?
+   (maybe-boolean #f)
+   "Likewise as the @code{allow-power-off-interrupts?} option, but for the
+  suspend action.")
+
+  (broadcast-power-off-interrupts?
+   (maybe-boolean #f)
+   "Whether an interrupt of a power-off action is broadcasted.")
+
+  (broadcast-suspend-interrupts?
+   (maybe-boolean #f)
+   "Whether an interrupt of a suspend action is broadcasted.")
+
+  ;; logind.conf options.
+  (kill-user-processes?
+   (maybe-boolean #f)
+   "Whether the processes of a user should be killed when the user logs
+  out.")
+
+  (kill-only-users
+   maybe-list-of-user-names
+   "Usernames whose processes should be killed, regardless the value of
+  @code{kill-user-processes?}.")
+
+  (kill-exclude-users
+   (maybe-list-of-user-names (list "root"))
+   "Usernames whose processes should @emph{not} be killed, regardless the
+  value of @code{kill-user-processes?}.")
+
+  (inhibit-delay-max-seconds
+   (maybe-non-negative-integer 5)
+   "The maximum time a system shutdown or sleep request is delayed due to an
+  inhibitor lock of type delay being active before the inhibitor is ignored and
+  the operation executes anyway.")
+
+  (handle-power-key
+   (maybe-action 'poweroff)
+   "The action done when the power key is pressed.  The compiled default is
+  @code{'poweroff}.")
+
+  (handle-suspend-key
+   (maybe-action 'suspend)
+   "The action done when the suspend key is pressed.  The ")
+
+  (handle-hibernate-key
+   (maybe-action 'hibernate)
+   "The action done when the hibernate key is pressed.")
+
+  (handle-lid-switch
+   (maybe-action 'suspend)
+   "The action done when the lid is closed.")
+
+  (handle-lid-switch-docked
+   (maybe-action 'ignore)
+   "The action done when the lid is closed and the device docked.")
+
+  (handle-lid-switch-external-power
+   (maybe-action 'suspend)
+   "The action done when the lid is closed and the device is externally
+  powered.")
+
+  (power-key-ignore-inhibited?
+   (maybe-boolean #f)
+   "Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+  idle) when the power key is pressed.")
+
+  (suspend-key-ignore-inhibited?
+   (maybe-boolean #f)
+   "Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+  idle) when the suspend key is pressed.")
+
+  (hibernate-key-ignore-inhibited?
+   (maybe-boolean #f)
+   "Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+  idle) when the hibernate key is pressed.")
+
+  (lid-switch-ignore-inhibited?
+   (maybe-boolean #f)
+   "Whether to ignore high-level inhibitor locks (shutdown, reboot, sleep or
+  idle) when the lid is closed.")
+
+  (holdoff-timeout-seconds
+   (maybe-non-negative-integer 30)
+   "Specifies the number of seconds after system startup or system resume
+during which elogind will hold off on reacting to lid events.")
+
+  (idle-action
+   (maybe-action 'ignore)
+   "Action to take when the system is idle.")
+
+  (idle-action-seconds
+   maybe-non-negative-integer
+   "The delay after which the action configured in @code{idle-action} is
+taken after the system is idle.")
+
+  ;; XXX: Perhaps deprecate in the future and handle all the accepted forms
+  ;; directly in 'runtime-directory-size' instead.
+  (runtime-directory-size-percent
+   maybe-percent
+   "Set the size limit, in percent, on the @env{XDG_RUNTIME_DIR} runtime
+directory for each user who logs in.  This specifies the per-user size limit
+relative to the amount of physical @acronym{RAM, read access memory}.  This
+value takes precedence over that specified via @code{runtime-directory-size}."
+   (serializer empty-serializer))       ;special cased at serialization time
+
+  (runtime-directory-size
+   maybe-non-negative-integer
+   "Set the size limit, in bytes, on the @env{XDG_RUNTIME_DIR} runtime
+directory for each user who logs in."
+   (serializer empty-serializer))       ;special cased at serialization time
+
+  (remove-ipc?
+   (maybe-boolean #t)
+   "Whether @acronym{IPC, inter-process communication} objects belonging to
+the user shall be removed when the user fully logs out.")
+
+  ;; sleep.conf options.
+  ;; CAUTION: all sleep.conf option names must be registered in the above
+  ;; %ELOGIND-CONFIGURATION-SLEEP-FIELDS variable: otherwise they will be
+  ;; serialized to logind.conf instead of sleep.conf!
+  (suspend-state
+   (maybe-list-of-suspend-states '(mem standby freeze))
+   "The suspend state values to be write to @file{/sys/power/state} by elogind
+  when suspending the system.  They will be tried in turn, until one is written
+  without error.")
+
+  (suspend-mode
+   (maybe-list-of-suspend-modes)
+   "The suspend mode values to write to @file{/sys/power/mem_sleep} by elogind
+  when suspending the system.")
+
+  (suspend-estimation-seconds
+   (maybe-non-negative-integer (* 60 60)) ;1 hour
+   "Cause the RTC alarm to wake the system after the specified time span to
+  measure the system battery capacity level and estimate the battery discharging
+  rate, which is used for estimating the time span until the system battery
+  charge level goes down to 5%.  This option is only used by elogind when using
+  the @code{'suspend-then-hibernate} action.")
+
+  (hibernate-mode
+   (maybe-list-of-hibernation-modes '(platform shutdown))
+   "The hibernation mode values to write to @file{/sys/power/disk} by elogind
+  when hibernating the system.")
+
+  (hibernate-delay-seconds
+   maybe-non-negative-integer
+   "The amount of time the system spends in suspend mode before the system is
+  automatically put into hibernate mode.")
+
+  ;; TODO: Remove in May 2026.
+  (hibernate-state
+   maybe-list-of-strings
+   "Deprecated option."
+   (serializer elogind-deprecated-empty-serializer))
+
+  ;; TODO: Remove in May 2026.
+  (hybrid-sleep-state
+   maybe-list-of-strings
+   "Deprecated option."
+   (serializer elogind-deprecated-empty-serializer))
+
+  ;; TODO: Remove in May 2026.
+  (hybrid-sleep-mode
+   maybe-list-of-strings
+   "Deprecated option."
+   (serializer elogind-deprecated-empty-serializer))
+
+  (prefix elogind-))
+
+(define (logind.conf config)
+  (let ((logind-fields (remove (lambda (field)
+                                 (memq (configuration-field-name field)
+                                       %elogind-configuration-sleep-fields))
+                               elogind-configuration-fields)))
+    (match-record config <elogind-configuration>
+                  (runtime-directory-size-percent runtime-directory-size)
+      ;; Handle the special-cased
+      ;; runtime-directory-size-percent/runtime-directory-size options pair.
+      (let ((runtime-directory-size
+             (if (maybe-value-set? runtime-directory-size-percent)
+                 (format #f "~a%~%" runtime-directory-size-percent) ;10 -> 10%
+                 runtime-directory-size)))
+        (mixed-text-file
+         "logind.conf"
+         "[Login]\n"
+         (if (maybe-value-set? runtime-directory-size)
+             (list "RuntimeDirectorySize=" runtime-directory-size)
+             "")
+         (serialize-configuration config logind-fields))))))
+
+(define (sleep.conf config)
+  (let ((sleep-fields (filter (lambda (field)
+                                (memq (configuration-field-name field)
+                                      %elogind-configuration-sleep-fields))
+                              elogind-configuration-fields)))
+    (mixed-text-file
+     "sleep.conf"
+     "[Sleep]\n"
+     (serialize-configuration config sleep-fields))))
 
 (define (elogind-etc-directory config)
   "Return the /etc/elogind directory for CONFIG."
@@ -1213,12 +1410,19 @@ (define (elogind-etc-directory config)
              (chmod dest #o500)))
 
          (mkdir-p #$output)            ;in case neither directory gets created
+
+         ;; Symlink the main configuration files.
+         (with-directory-excursion #$output
+           (symlink #$(logind.conf config) "logind.conf")
+           (symlink #$(sleep.conf config) "sleep.conf"))
+
          (for-each (lambda (f)
                      (copy-script f sleep-directory))
-                   '#$(elogind-system-sleep-hook-files config))
+                   '#$(elogind-configuration-system-sleep-hook-files config))
          (for-each (lambda (f)
                      (copy-script f shutdown-directory))
-                   '#$(elogind-system-shutdown-hook-files config))))))
+                   '#$(elogind-configuration-system-shutdown-hook-files
+                       config))))))
 
 (define (elogind-dbus-service config)
   "Return a @file{org.freedesktop.login1.service} file that tells D-Bus how to
@@ -1231,7 +1435,7 @@ (define (elogind-dbus-service config)
   ;; <https://issues.guix.gnu.org/55444>.
 
   (define elogind
-    (elogind-package config))
+    (elogind-configuration-elogind config))
 
   (define wrapper
     (program-file "elogind-dbus-shepherd-sync"
@@ -1288,7 +1492,7 @@ (define (pam-extension-procedure config)
   (define pam-elogind
     (pam-entry
      (control "required")
-     (module (file-append (elogind-package config)
+     (module (file-append (elogind-configuration-elogind config)
                           "/lib/security/pam_elogind.so"))))
 
   (list (pam-extension
@@ -1301,56 +1505,52 @@ (define (pam-extension-procedure config)
 
 (define (elogind-shepherd-service config)
   "Return a Shepherd service to start elogind according to @var{config}."
-  (define config-file
-    (elogind-configuration-file config))
-
   (list (shepherd-service
          (requirement '(user-processes dbus-system))
          (provision '(elogind))
          (start #~(make-forkexec-constructor
-                   (list #$(file-append (elogind-package config)
-                                        "/libexec/elogind/elogind"))
-                   #:environment-variables
-                   (list (string-append "ELOGIND_CONF_FILE="
-                                        #$config-file))))
+                   (list #$(file-append (elogind-configuration-elogind config)
+                                        "/libexec/elogind/elogind"))))
          (stop #~(make-kill-destructor))
-         (actions (list (shepherd-configuration-action config-file))))))
+         (actions (list (shepherd-configuration-action
+                         "/etc/elogind/logind.conf"))))))
 
 (define elogind-service-type
-  (service-type (name 'elogind)
-                (extensions
-                 (list (service-extension dbus-root-service-type
-                                          elogind-dbus-service)
-                       (service-extension udev-service-type
-                                          (compose list elogind-package))
-                       (service-extension polkit-service-type
-                                          (compose list elogind-package))
-
-                       ;; Start elogind from the Shepherd rather than waiting
-                       ;; for bus activation.  This ensures that it can handle
-                       ;; events like lid close, etc.
-                       (service-extension shepherd-root-service-type
-                                          elogind-shepherd-service)
-
-                       ;; Provide the 'loginctl' command.
-                       (service-extension profile-service-type
-                                          (compose list elogind-package))
-
-                       ;; Extend PAM with pam_elogind.so.
-                       (service-extension pam-root-service-type
-                                          pam-extension-procedure)
-
-                       ;; Install sleep/shutdown hook files.
-                       (service-extension etc-service-type
-                                          (lambda (config)
-                                            `(("elogind"
-                                               ,(elogind-etc-directory config)))))
-
-                       ;; We need /run/user, /run/systemd, etc.
-                       (service-extension file-system-service-type
-                                          (const %elogind-file-systems))))
-                (default-value (elogind-configuration))
-                (description "Run the @command{elogind} login and seat
+  (service-type
+   (name 'elogind)
+   (extensions
+    (list (service-extension dbus-root-service-type
+                             elogind-dbus-service)
+          (service-extension udev-service-type
+                             (compose list elogind-configuration-elogind))
+          (service-extension polkit-service-type
+                             (compose list elogind-configuration-elogind))
+
+          ;; Start elogind from the Shepherd rather than waiting
+          ;; for bus activation.  This ensures that it can handle
+          ;; events like lid close, etc.
+          (service-extension shepherd-root-service-type
+                             elogind-shepherd-service)
+
+          ;; Provide the 'loginctl' command.
+          (service-extension profile-service-type
+                             (compose list elogind-configuration-elogind))
+
+          ;; Extend PAM with pam_elogind.so.
+          (service-extension pam-root-service-type
+                             pam-extension-procedure)
+
+          ;; Install sleep/shutdown hook files.
+          (service-extension etc-service-type
+                             (lambda (config)
+                               `(("elogind"
+                                  ,(elogind-etc-directory config)))))
+
+          ;; We need /run/user, /run/systemd, etc.
+          (service-extension file-system-service-type
+                             (const %elogind-file-systems))))
+   (default-value (elogind-configuration))
+   (description "Run the @command{elogind} login and seat
 management service.  The @command{elogind} service integrates with PAM to
 allow other system components to know the set of logged-in users as well as
 their session types (graphical, console, remote, etc.).  It can also clean up

base-commit: 5b981a2b5c9c880634d883f3b7ae078b4fa75fdb
-- 
2.49.0