From 281e9398cc94cad3e5fc4206fefb83e7bb166476 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 17 Nov 2025 22:04:08 +0100 Subject: [PATCH] swayidle: improve confusing "events" configuration With swayidle one can configure two different kinds of hooks: - Idle timeouts are executed after the session has been idle for a specific amount of time. - Events are executed when systemd notifies us that for example the user session is locked or that the device is about to suspend. While not obvious, there is a significant difference between how these two kinds are configured: there can be several timeouts with separate commands to be executed, but each event can only be specified once. If an event is specified multiple times, then the last command wins. This can be very easy to miss in swayidle's documentation. Furthermore, because the config is a list of `{ event = "..."; command = "..."; }` attrset, we double down on this confusion and make it seem like having multiple handlers for an event was actually supported. Fix this by converting from a list of "event" submodules to an attrset where the key is the event name and the value is the command to be executed. This makes it impossible to specify multiple commands for a single event by accident. If a user _does_ want to have multiple commands executed on any event they can for example use `pkgs.writeShellScript` and manually chain the commands in that script. --- modules/services/swayidle.nix | 73 +++++++++++++------ .../services/swayidle/basic-configuration.nix | 14 +--- tests/modules/services/swayidle/default.nix | 1 + .../swayidle/legacy-configuration.nix | 51 +++++++++++++ 4 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 tests/modules/services/swayidle/legacy-configuration.nix diff --git a/modules/services/swayidle.nix b/modules/services/swayidle.nix index 481dd4406..5af0b4678 100644 --- a/modules/services/swayidle.nix +++ b/modules/services/swayidle.nix @@ -42,21 +42,30 @@ in }; }; - eventModule = { + eventsModule = { options = { - event = mkOption { - type = types.enum [ - "before-sleep" - "after-resume" - "lock" - "unlock" - ]; - description = "Event name."; + before-sleep = mkOption { + type = types.nullOr types.str; + default = null; + description = "Command to run before suspending."; }; - command = mkOption { - type = types.str; - description = "Command to run when event occurs."; + after-resume = mkOption { + type = types.nullOr types.str; + default = null; + description = "Command to run after resuming."; + }; + + lock = mkOption { + type = types.nullOr types.str; + default = null; + description = "Command to run when the logind session is locked."; + }; + + unlock = mkOption { + type = types.nullOr types.str; + default = null; + description = "Command to run when the logind session is unlocked."; }; }; }; @@ -80,13 +89,31 @@ in }; events = mkOption { - type = with types; listOf (submodule eventModule); + type = + with types; + (coercedTo (listOf attrs)) ( + events: + lib.warn + '' + The syntax of services.swayidle.events has changed. While it + previously accepted a list of events, it now accepts an attrset + keyed by the event name. + '' + ( + lib.listToAttrs ( + map (e: { + name = e.event; + value = e.command; + }) events + ) + ) + ) (submodule eventsModule); default = [ ]; example = literalExpression '' - [ - { event = "before-sleep"; command = "''${pkgs.swaylock}/bin/swaylock -fF"; } - { event = "lock"; command = "lock"; } - ] + { + "before-sleep" = "''${pkgs.swaylock}/bin/swaylock -fF"; + "lock" = "lock"; + } ''; description = "Run command on occurrence of a event."; }; @@ -144,13 +171,17 @@ in t.resumeCommand ]; - mkEvent = e: [ - e.event - e.command + mkEvent = event: command: [ + event + command ]; + nonemptyEvents = lib.filterAttrs (event: command: command != null) cfg.events; + args = - cfg.extraArgs ++ (lib.concatMap mkTimeout cfg.timeouts) ++ (lib.concatMap mkEvent cfg.events); + cfg.extraArgs + ++ (lib.concatMap mkTimeout cfg.timeouts) + ++ (lib.flatten (lib.mapAttrsToList mkEvent nonemptyEvents)); in "${lib.getExe cfg.package} ${lib.escapeShellArgs args}"; }; diff --git a/tests/modules/services/swayidle/basic-configuration.nix b/tests/modules/services/swayidle/basic-configuration.nix index b5534c75a..5da059b1d 100644 --- a/tests/modules/services/swayidle/basic-configuration.nix +++ b/tests/modules/services/swayidle/basic-configuration.nix @@ -19,16 +19,10 @@ resumeCommand = ''swaymsg "output * dpms on"''; } ]; - events = [ - { - event = "before-sleep"; - command = "swaylock -fF"; - } - { - event = "lock"; - command = "swaylock -fF"; - } - ]; + events = { + before-sleep = "swaylock -fF"; + lock = "swaylock -fF"; + }; }; nmt.script = '' diff --git a/tests/modules/services/swayidle/default.nix b/tests/modules/services/swayidle/default.nix index 62db035ba..75c88d284 100644 --- a/tests/modules/services/swayidle/default.nix +++ b/tests/modules/services/swayidle/default.nix @@ -2,4 +2,5 @@ lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { swayidle-basic-configuration = ./basic-configuration.nix; + swayidle-legacy-configuration = ./legacy-configuration.nix; } diff --git a/tests/modules/services/swayidle/legacy-configuration.nix b/tests/modules/services/swayidle/legacy-configuration.nix new file mode 100644 index 000000000..e07834fa7 --- /dev/null +++ b/tests/modules/services/swayidle/legacy-configuration.nix @@ -0,0 +1,51 @@ +{ config, ... }: +{ + services.swayidle = { + enable = true; + package = config.lib.test.mkStubPackage { outPath = "@swayidle@"; }; + events = [ + { + event = "lock"; + command = "swaylock -fF"; + } + { + event = "before-sleep"; + command = "swaylock -fF"; + } + ]; + }; + + test.asserts.evalWarnings.expected = [ + '' + The syntax of services.swayidle.events has changed. While it + previously accepted a list of events, it now accepts an attrset + keyed by the event name. + '' + ]; + + nmt.script = '' + serviceFile=home-files/.config/systemd/user/swayidle.service + + assertFileExists "$serviceFile" + + serviceFileNormalized="$(normalizeStorePaths "$serviceFile")" + + assertFileContent "$serviceFileNormalized" ${builtins.toFile "expected.service" '' + [Install] + WantedBy=graphical-session.target + + [Service] + Environment=PATH=@bash-interactive@/bin + ExecStart=@swayidle@/bin/dummy -w before-sleep 'swaylock -fF' lock 'swaylock -fF' + Restart=always + Type=simple + + [Unit] + After=graphical-session.target + ConditionEnvironment=WAYLAND_DISPLAY + Description=Idle manager for Wayland + Documentation=man:swayidle(1) + PartOf=graphical-session.target + ''} + ''; +}