From 183e8f957057886c828c2e61459c7c673a5dbfe7 Mon Sep 17 00:00:00 2001 From: "home-manager-ci[bot]" <214323736+home-manager-ci[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 04:11:14 +0000 Subject: [PATCH 01/47] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce?narHash=sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c%3D' (2025-10-25) → 'github:NixOS/nixpkgs/2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15?narHash=sha256-kJ8lIZsiPOmbkJypG%2BB5sReDXSD1KGu2VEPNqhRa/ew%3D' (2025-10-31) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 94bff35ca..0a8f512ba 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1761373498, - "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "lastModified": 1761907660, + "narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", "type": "github" }, "original": { From 363797f8a94d703277ceae122e7c41abc07e32a2 Mon Sep 17 00:00:00 2001 From: pancho horrillo Date: Sun, 2 Nov 2025 06:28:18 +0100 Subject: [PATCH 02/47] gpg-agent: fix syntax-breaking extraneous new line --- modules/services/gpg-agent.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/services/gpg-agent.nix b/modules/services/gpg-agent.nix index d4c27e91f..e86a39220 100644 --- a/modules/services/gpg-agent.nix +++ b/modules/services/gpg-agent.nix @@ -19,9 +19,7 @@ let inherit (config.programs.gpg) homedir; - gpgSshSupportStr = '' - ${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye - ''; + gpgSshSupportStr = "${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye"; gpgBashInitStr = '' GPG_TTY="$(tty)" From 5eaa0072ff2e74d235aa6b010b6cd32f61dcf161 Mon Sep 17 00:00:00 2001 From: pancho horrillo Date: Sun, 2 Nov 2025 07:37:46 +0100 Subject: [PATCH 03/47] gpg-agent: restore empty newlines after commands --- modules/services/gpg-agent.nix | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/services/gpg-agent.nix b/modules/services/gpg-agent.nix index e86a39220..e2ef852f2 100644 --- a/modules/services/gpg-agent.nix +++ b/modules/services/gpg-agent.nix @@ -25,17 +25,23 @@ let GPG_TTY="$(tty)" export GPG_TTY '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgZshInitStr = '' export GPG_TTY=$TTY '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgFishInitStr = '' set -gx GPG_TTY (tty) '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgNushellInitStr = '' $env.GPG_TTY = (tty) From 43e205606aeb253bfcee15fd8a4a01d8ce8384ca Mon Sep 17 00:00:00 2001 From: Yechiel Worenklein <41305372+yechielw@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:21:11 +0200 Subject: [PATCH 04/47] cbatticon: add `package` example After batticonplus was merged into nixpkgs --- modules/services/cbatticon.nix | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/services/cbatticon.nix b/modules/services/cbatticon.nix index c441f1799..70daa9fc3 100644 --- a/modules/services/cbatticon.nix +++ b/modules/services/cbatticon.nix @@ -42,7 +42,13 @@ in services.cbatticon = { enable = lib.mkEnableOption "cbatticon"; - package = lib.mkPackageOption pkgs "cbatticon" { }; + package = lib.mkPackageOption pkgs "cbatticon" { + example = "pkgs.batticonplus"; + extraDescription = '' + Use {var}`pkgs.batticonplus` + for wayland support. + ''; + }; commandCriticalLevel = mkOption { type = types.nullOr types.lines; From 371608e69cb7ffc92321555ec71aada6edeee429 Mon Sep 17 00:00:00 2001 From: Robert Radestock Date: Mon, 3 Nov 2025 04:52:56 +0100 Subject: [PATCH 05/47] rclone: add option to set log-level (#8105) Add an option to set rclone's log-level per mount: programs.rclone.remotes..mounts..logLevel = "DEBUG"; If no value is set, it'll use rclone's implicit default ("NOTICE") Previously, the debug log-level got enforced (via "-vv"), which caused noisy logs, and there was no easy way to change that. Note: rclone global-flags can't be configured in the config file, so this uses the environment variable approach. references: - https://rclone.org/docs/#logging - https://rclone.org/docs/#v-vv-verbose If no value is given, use the implicit default of rclone instead of redefining it through the options --- modules/programs/rclone.nix | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/programs/rclone.nix b/modules/programs/rclone.nix index 9e92609bb..40fd5790f 100644 --- a/modules/programs/rclone.nix +++ b/modules/programs/rclone.nix @@ -111,6 +111,23 @@ in options = { enable = lib.mkEnableOption "this mount"; + logLevel = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "ERROR" + "NOTICE" + "INFO" + "DEBUG" + ] + ); + default = null; + example = "INFO"; + description = '' + Set the log-level. + See: https://rclone.org/docs/#logging + ''; + }; + mountPoint = lib.mkOption { type = lib.types.str; default = null; @@ -348,12 +365,13 @@ in Environment = [ # fusermount/fusermount3 "PATH=/run/wrappers/bin" - ]; + ] + ++ lib.optional (mount.logLevel != null) "RCLONE_LOG_LEVEL=${mount.logLevel}"; + ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mount.mountPoint}"; ExecStart = lib.concatStringsSep " " [ (lib.getExe cfg.package) "mount" - "-vv" (lib.cli.toGNUCommandLineShell { } mount.options) "${remote-name}:${mount-path}" "${mount.mountPoint}" From 0fe68257a9f80d469b2c8457f02b8d36174b1020 Mon Sep 17 00:00:00 2001 From: Raul Rodrigues de Oliveira Date: Mon, 3 Nov 2025 00:53:28 -0300 Subject: [PATCH 06/47] fish: added repaint to binds (#8113) --- modules/programs/fish.nix | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/programs/fish.nix b/modules/programs/fish.nix index 006fb53d1..49922ef50 100644 --- a/modules/programs/fish.nix +++ b/modules/programs/fish.nix @@ -261,6 +261,7 @@ let }; erase = mkEnableOption "remove bind"; silent = mkEnableOption "Operate silently"; + repaint = mkEnableOption "redraw prompt after command"; operate = mkOption { description = "Operate on preset bindings or user bindings"; type = @@ -324,6 +325,7 @@ let { silent, erase, + repaint, operate, mode, setsMode, @@ -344,7 +346,11 @@ let ]; cmdNormal = lib.concatStringsSep " " ( - [ "bind" ] ++ opts ++ [ k ] ++ map lib.escapeShellArg (lib.flatten [ command ]) + [ "bind" ] + ++ opts + ++ [ k ] + ++ map lib.escapeShellArg (lib.flatten [ command ]) + ++ lib.optional repaint "repaint" ); cmdErase = lib.concatStringsSep " " ( From 50a5766d5158309c7ff1f52fc6edcc32ee480bc0 Mon Sep 17 00:00:00 2001 From: Alex Ionescu Date: Mon, 3 Nov 2025 04:55:45 +0100 Subject: [PATCH 07/47] kitty: add option `mouseBindings` (#8111) --- modules/programs/kitty.nix | 19 ++++++++++++++++++- .../kitty/example-mkOrder-expected.conf | 3 +++ .../programs/kitty/example-mkOrder.nix | 5 +++++ .../kitty/example-settings-expected.conf | 3 +++ .../programs/kitty/example-settings.nix | 5 +++++ .../kitty/null-shellIntegration-expected.conf | 1 + 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/modules/programs/kitty.nix b/modules/programs/kitty.nix index b8f0157a0..96087ae78 100644 --- a/modules/programs/kitty.nix +++ b/modules/programs/kitty.nix @@ -42,6 +42,10 @@ let mkKeyValue = key: command: "map ${key} ${command}"; }; + toKittyMouseBindings = lib.generators.toKeyValue { + mkKeyValue = key: command: "mouse_map ${key} ${command}"; + }; + toKittyActionAliases = lib.generators.toKeyValue { mkKeyValue = alias_name: action: "action_alias ${alias_name} ${action}"; }; @@ -199,6 +203,18 @@ in ''; }; + mouseBindings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Mapping of mouse bindings to actions."; + example = literalExpression '' + { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + ''; + }; + environment = mkOption { type = types.attrsOf types.str; default = { }; @@ -316,7 +332,8 @@ in (mkOrder 540 (toKittyConfig cfg.settings)) (mkOrder 550 (toKittyActionAliases cfg.actionAliases)) (mkOrder 560 (toKittyKeybindings cfg.keybindings)) - (mkOrder 570 (toKittyEnv cfg.environment)) + (mkOrder 570 (toKittyMouseBindings cfg.mouseBindings)) + (mkOrder 580 (toKittyEnv cfg.environment)) ]; xdg.configFile."kitty/kitty.conf" = { diff --git a/tests/modules/programs/kitty/example-mkOrder-expected.conf b/tests/modules/programs/kitty/example-mkOrder-expected.conf index 76ee85246..111b93aaa 100644 --- a/tests/modules/programs/kitty/example-mkOrder-expected.conf +++ b/tests/modules/programs/kitty/example-mkOrder-expected.conf @@ -19,5 +19,8 @@ action_alias launch_window launch --cwd=current --type=os-window map ctrl+c copy_or_interrupt map ctrl+f>2 set_font_size 20 +mouse_map ctrl+left click ungrabbed mouse_handle_click selection link prompt +mouse_map left click ungrabbed no-op + env LS_COLORS=1 diff --git a/tests/modules/programs/kitty/example-mkOrder.nix b/tests/modules/programs/kitty/example-mkOrder.nix index acc58287f..53dbd468b 100644 --- a/tests/modules/programs/kitty/example-mkOrder.nix +++ b/tests/modules/programs/kitty/example-mkOrder.nix @@ -25,6 +25,11 @@ "ctrl+f>2" = "set_font_size 20"; }; + mouseBindings = { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + actionAliases = { "launch_tab" = "launch --cwd=current --type=tab"; "launch_window" = "launch --cwd=current --type=os-window"; diff --git a/tests/modules/programs/kitty/example-settings-expected.conf b/tests/modules/programs/kitty/example-settings-expected.conf index 0a6afc926..0a039ac82 100644 --- a/tests/modules/programs/kitty/example-settings-expected.conf +++ b/tests/modules/programs/kitty/example-settings-expected.conf @@ -17,5 +17,8 @@ action_alias launch_window launch --cwd=current --type=os-window map ctrl+c copy_or_interrupt map ctrl+f>2 set_font_size 20 +mouse_map ctrl+left click ungrabbed mouse_handle_click selection link prompt +mouse_map left click ungrabbed no-op + env LS_COLORS=1 diff --git a/tests/modules/programs/kitty/example-settings.nix b/tests/modules/programs/kitty/example-settings.nix index 484dacda6..5383f2525 100644 --- a/tests/modules/programs/kitty/example-settings.nix +++ b/tests/modules/programs/kitty/example-settings.nix @@ -25,6 +25,11 @@ "ctrl+f>2" = "set_font_size 20"; }; + mouseBindings = { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + actionAliases = { "launch_tab" = "launch --cwd=current --type=tab"; "launch_window" = "launch --cwd=current --type=os-window"; diff --git a/tests/modules/programs/kitty/null-shellIntegration-expected.conf b/tests/modules/programs/kitty/null-shellIntegration-expected.conf index fedba1157..c1bcc986a 100644 --- a/tests/modules/programs/kitty/null-shellIntegration-expected.conf +++ b/tests/modules/programs/kitty/null-shellIntegration-expected.conf @@ -4,3 +4,4 @@ + From 61f2cc59089d48c85d761c0c94388e2dc421b712 Mon Sep 17 00:00:00 2001 From: Ilya Savitsky Date: Mon, 3 Nov 2025 04:04:44 +0000 Subject: [PATCH 08/47] local-ai: init module (#6718) --- .../misc/news/2025/10/2025-10-31_12-00-00.nix | 9 ++++ modules/services/local-ai.nix | 48 +++++++++++++++++++ tests/modules/services/local-ai/default.nix | 6 +++ .../local-ai/enabled-with-environment.nix | 20 ++++++++ tests/modules/services/local-ai/enabled.nix | 9 ++++ 5 files changed, 92 insertions(+) create mode 100644 modules/misc/news/2025/10/2025-10-31_12-00-00.nix create mode 100644 modules/services/local-ai.nix create mode 100644 tests/modules/services/local-ai/default.nix create mode 100644 tests/modules/services/local-ai/enabled-with-environment.nix create mode 100644 tests/modules/services/local-ai/enabled.nix diff --git a/modules/misc/news/2025/10/2025-10-31_12-00-00.nix b/modules/misc/news/2025/10/2025-10-31_12-00-00.nix new file mode 100644 index 000000000..179586a35 --- /dev/null +++ b/modules/misc/news/2025/10/2025-10-31_12-00-00.nix @@ -0,0 +1,9 @@ +{ + time = "2025-10-31T12:00:00+00:00"; + condition = true; + message = '' + services.local-ai: new module + + Added LocalAI, a free, Open Source OpenAI alternative. + ''; +} diff --git a/modules/services/local-ai.nix b/modules/services/local-ai.nix new file mode 100644 index 000000000..94cea9c65 --- /dev/null +++ b/modules/services/local-ai.nix @@ -0,0 +1,48 @@ +{ + pkgs, + config, + lib, + ... +}: + +let + inherit (lib) types; + cfg = config.services.local-ai; +in +{ + meta.maintainers = [ lib.maintainers.ipsavitsky ]; + + options.services.local-ai = { + enable = lib.mkEnableOption "LocalAI is the free, Open Source OpenAI alternative."; + + package = lib.mkPackageOption pkgs "local-ai" { }; + + environment = lib.mkOption { + type = types.attrsOf types.string; + default = { }; + description = '' + Additional environment passed to local-ai service. Used to configure local-ai + + See for available options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.user.services.local-ai = { + Unit = { + Description = "Server for local large language models"; + After = [ "network.target" ]; + }; + + Service = { + ExecStart = lib.getExe cfg.package; + Environment = lib.mapAttrsToList (key: val: "${key}=${val}") cfg.environment; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} diff --git a/tests/modules/services/local-ai/default.nix b/tests/modules/services/local-ai/default.nix new file mode 100644 index 000000000..bc67d7482 --- /dev/null +++ b/tests/modules/services/local-ai/default.nix @@ -0,0 +1,6 @@ +{ lib, pkgs, ... }: + +lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + local-ai-enabled = ./enabled.nix; + local-ai-enabled-with-environment = ./enabled-with-environment.nix; +} diff --git a/tests/modules/services/local-ai/enabled-with-environment.nix b/tests/modules/services/local-ai/enabled-with-environment.nix new file mode 100644 index 000000000..aaf7c8f7c --- /dev/null +++ b/tests/modules/services/local-ai/enabled-with-environment.nix @@ -0,0 +1,20 @@ +{ ... }: + +{ + services.local-ai = { + enable = true; + environment = { + MODELS_PATH = "/tmp/models"; + PRELOAD_MODELS = "[{ \"url\": \"https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf\", \"name\": \"mistral-7b-instruct-v0.2.Q4_K_M.gguf\" }]"; + }; + }; + + nmt.script = '' + assertFileContains \ + home-files/.config/systemd/user/local-ai.service \ + "Environment=MODELS_PATH=/tmp/models" + assertFileContains \ + home-files/.config/systemd/user/local-ai.service \ + "Environment=PRELOAD_MODELS=[{ \"url\": \"https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf\", \"name\": \"mistral-7b-instruct-v0.2.Q4_K_M.gguf\" }]" + ''; +} diff --git a/tests/modules/services/local-ai/enabled.nix b/tests/modules/services/local-ai/enabled.nix new file mode 100644 index 000000000..9c75319f6 --- /dev/null +++ b/tests/modules/services/local-ai/enabled.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + services.local-ai.enable = true; + + nmt.script = '' + assertFileExists home-files/.config/systemd/user/local-ai.service + ''; +} From d9cd40d2daf03350b61f853653afb53625e01a80 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:41:08 -0600 Subject: [PATCH 09/47] local-ai: string -> str (#8116) Signed-off-by: Austin Horstman --- modules/services/local-ai.nix | 2 +- tests/modules/services/local-ai/enabled-with-environment.nix | 2 -- tests/modules/services/local-ai/enabled.nix | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/services/local-ai.nix b/modules/services/local-ai.nix index 94cea9c65..a95cfdfbd 100644 --- a/modules/services/local-ai.nix +++ b/modules/services/local-ai.nix @@ -18,7 +18,7 @@ in package = lib.mkPackageOption pkgs "local-ai" { }; environment = lib.mkOption { - type = types.attrsOf types.string; + type = types.attrsOf types.str; default = { }; description = '' Additional environment passed to local-ai service. Used to configure local-ai diff --git a/tests/modules/services/local-ai/enabled-with-environment.nix b/tests/modules/services/local-ai/enabled-with-environment.nix index aaf7c8f7c..ed744fd90 100644 --- a/tests/modules/services/local-ai/enabled-with-environment.nix +++ b/tests/modules/services/local-ai/enabled-with-environment.nix @@ -1,5 +1,3 @@ -{ ... }: - { services.local-ai = { enable = true; diff --git a/tests/modules/services/local-ai/enabled.nix b/tests/modules/services/local-ai/enabled.nix index 9c75319f6..1d1100ac6 100644 --- a/tests/modules/services/local-ai/enabled.nix +++ b/tests/modules/services/local-ai/enabled.nix @@ -1,5 +1,3 @@ -{ ... }: - { services.local-ai.enable = true; From ee4befe0dd7081520965f5b5e4f5d27331e21dc6 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:51:52 -0600 Subject: [PATCH 10/47] darwinScrublist: taskwarrior rename Signed-off-by: Austin Horstman --- tests/darwinScrublist.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/darwinScrublist.nix b/tests/darwinScrublist.nix index 1287f6812..ff23a728f 100644 --- a/tests/darwinScrublist.nix +++ b/tests/darwinScrublist.nix @@ -157,7 +157,7 @@ let "spotify-player" "starship" "superfile" - "taskwarrior" + "taskwarrior2" "tealdeer" "texlive" "thefuck" From 183bcb80b988412b37e77bfb1b876b066f6b7976 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:52:47 -0600 Subject: [PATCH 11/47] taskwarrior: taskwarrior -> taskwarrior2 Signed-off-by: Austin Horstman --- modules/programs/taskwarrior.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/programs/taskwarrior.nix b/modules/programs/taskwarrior.nix index 9b5bdaa27..c7b8de1d2 100644 --- a/modules/programs/taskwarrior.nix +++ b/modules/programs/taskwarrior.nix @@ -87,7 +87,7 @@ in ''; }; - package = lib.mkPackageOption pkgs "taskwarrior" { + package = lib.mkPackageOption pkgs "taskwarrior2" { nullable = true; example = "pkgs.taskwarrior3"; }; From 9a26ccef6f130d07c5e121f1062955c02457fb2c Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:52:52 -0600 Subject: [PATCH 12/47] taskwarrior-sync: taskwarrior -> taskwarrior2 Signed-off-by: Austin Horstman --- modules/services/taskwarrior-sync.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/services/taskwarrior-sync.nix b/modules/services/taskwarrior-sync.nix index 959019b87..ab4fc440c 100644 --- a/modules/services/taskwarrior-sync.nix +++ b/modules/services/taskwarrior-sync.nix @@ -19,7 +19,7 @@ in options.services.taskwarrior-sync = { enable = lib.mkEnableOption "Taskwarrior periodic sync"; - package = lib.mkPackageOption pkgs "taskwarrior" { example = "pkgs.taskwarrior3"; }; + package = lib.mkPackageOption pkgs "taskwarrior2" { example = "pkgs.taskwarrior3"; }; frequency = lib.mkOption { type = lib.types.str; From e7d7e76bcd1896023015b57fb03f4a28d3090459 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:54:56 -0600 Subject: [PATCH 13/47] tests/darkman: python -> python2 Signed-off-by: Austin Horstman --- tests/modules/services/darkman/basic-configuration.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/services/darkman/basic-configuration.nix b/tests/modules/services/darkman/basic-configuration.nix index dbc89ac94..00a0fad0a 100644 --- a/tests/modules/services/darkman/basic-configuration.nix +++ b/tests/modules/services/darkman/basic-configuration.nix @@ -17,7 +17,7 @@ ''; lightModeScripts.color-scheme-light = pkgs.writeScript "my-python-script" '' - #!${pkgs.python}/bin/python + #!${pkgs.python2}/bin/python print('Do something!') ''; From a179f834adc0d8510ce20b7a872cb7b81b9d599d Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:56:40 -0600 Subject: [PATCH 14/47] tests/gtk: ubuntu_font_family -> ubuntu-classic Signed-off-by: Austin Horstman --- tests/modules/misc/gtk/gtk-global-inheritance.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/misc/gtk/gtk-global-inheritance.nix b/tests/modules/misc/gtk/gtk-global-inheritance.nix index 3d01f4843..d4c88af9c 100644 --- a/tests/modules/misc/gtk/gtk-global-inheritance.nix +++ b/tests/modules/misc/gtk/gtk-global-inheritance.nix @@ -5,7 +5,7 @@ font = { name = "Ubuntu"; size = 12; - package = pkgs.ubuntu_font_family; + package = pkgs.ubuntu-classic; }; theme = { name = "Adwaita-dark"; From 983cbdc75c6808b8162c539cdda3c1e4edf5bc61 Mon Sep 17 00:00:00 2001 From: "home-manager-ci[bot]" <214323736+home-manager-ci[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 04:11:14 +0000 Subject: [PATCH 15/47] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce?narHash=sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c%3D' (2025-10-25) → 'github:NixOS/nixpkgs/2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15?narHash=sha256-kJ8lIZsiPOmbkJypG%2BB5sReDXSD1KGu2VEPNqhRa/ew%3D' (2025-10-31) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 94bff35ca..0a8f512ba 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1761373498, - "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "lastModified": 1761907660, + "narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", "type": "github" }, "original": { From acf7743c895187d3d7f9a58173c01fd6bdb43f13 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:51:52 -0600 Subject: [PATCH 16/47] darwinScrublist: taskwarrior rename Signed-off-by: Austin Horstman --- tests/darwinScrublist.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/darwinScrublist.nix b/tests/darwinScrublist.nix index 1287f6812..ff23a728f 100644 --- a/tests/darwinScrublist.nix +++ b/tests/darwinScrublist.nix @@ -157,7 +157,7 @@ let "spotify-player" "starship" "superfile" - "taskwarrior" + "taskwarrior2" "tealdeer" "texlive" "thefuck" From 9f3a82bfd1e17b7b0e5803b8c0d48f73859bd555 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:52:47 -0600 Subject: [PATCH 17/47] taskwarrior: taskwarrior -> taskwarrior2 Signed-off-by: Austin Horstman --- modules/programs/taskwarrior.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/programs/taskwarrior.nix b/modules/programs/taskwarrior.nix index 9b5bdaa27..c7b8de1d2 100644 --- a/modules/programs/taskwarrior.nix +++ b/modules/programs/taskwarrior.nix @@ -87,7 +87,7 @@ in ''; }; - package = lib.mkPackageOption pkgs "taskwarrior" { + package = lib.mkPackageOption pkgs "taskwarrior2" { nullable = true; example = "pkgs.taskwarrior3"; }; From 9901bb6afc1a128d236ebae30975d08d14b08d04 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:52:52 -0600 Subject: [PATCH 18/47] taskwarrior-sync: taskwarrior -> taskwarrior2 Signed-off-by: Austin Horstman --- modules/services/taskwarrior-sync.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/services/taskwarrior-sync.nix b/modules/services/taskwarrior-sync.nix index 959019b87..ab4fc440c 100644 --- a/modules/services/taskwarrior-sync.nix +++ b/modules/services/taskwarrior-sync.nix @@ -19,7 +19,7 @@ in options.services.taskwarrior-sync = { enable = lib.mkEnableOption "Taskwarrior periodic sync"; - package = lib.mkPackageOption pkgs "taskwarrior" { example = "pkgs.taskwarrior3"; }; + package = lib.mkPackageOption pkgs "taskwarrior2" { example = "pkgs.taskwarrior3"; }; frequency = lib.mkOption { type = lib.types.str; From ab0d3db1aa32a8c18807a0d0115e7f20351d2a10 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:54:56 -0600 Subject: [PATCH 19/47] tests/darkman: python -> python2 Signed-off-by: Austin Horstman --- tests/modules/services/darkman/basic-configuration.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/services/darkman/basic-configuration.nix b/tests/modules/services/darkman/basic-configuration.nix index dbc89ac94..00a0fad0a 100644 --- a/tests/modules/services/darkman/basic-configuration.nix +++ b/tests/modules/services/darkman/basic-configuration.nix @@ -17,7 +17,7 @@ ''; lightModeScripts.color-scheme-light = pkgs.writeScript "my-python-script" '' - #!${pkgs.python}/bin/python + #!${pkgs.python2}/bin/python print('Do something!') ''; From 32a671dce5a045f0d15a0e9fae9eaf6a56d3bdaa Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 2 Nov 2025 22:56:40 -0600 Subject: [PATCH 20/47] tests/gtk: ubuntu_font_family -> ubuntu-classic Signed-off-by: Austin Horstman --- tests/modules/misc/gtk/gtk-global-inheritance.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/misc/gtk/gtk-global-inheritance.nix b/tests/modules/misc/gtk/gtk-global-inheritance.nix index 3d01f4843..d4c88af9c 100644 --- a/tests/modules/misc/gtk/gtk-global-inheritance.nix +++ b/tests/modules/misc/gtk/gtk-global-inheritance.nix @@ -5,7 +5,7 @@ font = { name = "Ubuntu"; size = 12; - package = pkgs.ubuntu_font_family; + package = pkgs.ubuntu-classic; }; theme = { name = "Adwaita-dark"; From 090aa14e5dbaa73f16624f408977582869c0c49a Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Thu, 30 Oct 2025 22:25:31 +0100 Subject: [PATCH 21/47] tmpfiles: migrate to an RFC42-style option --- modules/misc/tmpfiles.nix | 198 ++++++++++++++++-- modules/programs/glab.nix | 14 +- tests/default.nix | 1 + .../standalone/rclone/sops-nix.nix | 9 +- tests/modules/misc/tmpfiles/basic-rules.nix | 24 +++ tests/modules/misc/tmpfiles/common-stubs.nix | 3 + tests/modules/misc/tmpfiles/default.nix | 6 + .../tmpfiles/escaped-argument-warning.nix | 14 ++ tests/modules/misc/tmpfiles/no-rules.nix | 9 + 9 files changed, 247 insertions(+), 31 deletions(-) create mode 100644 tests/modules/misc/tmpfiles/basic-rules.nix create mode 100644 tests/modules/misc/tmpfiles/common-stubs.nix create mode 100644 tests/modules/misc/tmpfiles/default.nix create mode 100644 tests/modules/misc/tmpfiles/escaped-argument-warning.nix create mode 100644 tests/modules/misc/tmpfiles/no-rules.nix diff --git a/modules/misc/tmpfiles.nix b/modules/misc/tmpfiles.nix index b0d4c681e..82b791d89 100644 --- a/modules/misc/tmpfiles.nix +++ b/modules/misc/tmpfiles.nix @@ -9,38 +9,193 @@ let cfg = config.systemd.user.tmpfiles; + ruleSubmodule = lib.types.submodule ( + { name, ... }: + { + options.type = lib.mkOption { + type = lib.types.str; + readOnly = true; + default = name; + defaultText = "‹tmpfiles-type›"; + example = "d"; + description = '' + The type of operation to perform on the file. + + The type consists of a single letter and optionally one or more + modifier characters. + + Please see the upstream documentation for the available types and + more details: {manpage}`tmpfiles.d(5)` + ''; + }; + options.mode = lib.mkOption { + type = lib.types.str; + default = "-"; + example = "0755"; + description = '' + The file access mode to use when creating this file or directory. + ''; + }; + options.user = lib.mkOption { + type = lib.types.str; + default = "-"; + example = "root"; + description = '' + The user of the file. + + This may either be a numeric ID or a user/group name. + + If omitted or when set to `"-"`, the user and group of the user who + invokes systemd-tmpfiles is used. + ''; + }; + options.group = lib.mkOption { + type = lib.types.str; + default = "-"; + example = "root"; + description = '' + The group of the file. + + This may either be a numeric ID or a user/group name. + + If omitted or when set to `"-"`, the user and group of the user who + invokes systemd-tmpfiles is used. + ''; + }; + options.age = lib.mkOption { + type = lib.types.str; + default = "-"; + example = "10d"; + description = '' + Delete a file when it reaches a certain age. + + If a file or directory is older than the current time minus the age + field, it is deleted. + + If set to `"-"`, no automatic clean-up is done. + ''; + }; + options.argument = lib.mkOption { + type = lib.types.str; + default = ""; + example = ""; + description = '' + An argument whose meaning depends on the type of operation. + + Please see the upstream documentation for the meaning of this + parameter in different situations: {manpage}`tmpfiles.d(5)` + ''; + }; + } + ); + + modulePrefix = [ + "systemd" + "user" + "tmpfiles" + "settings" + ]; + + mkFileName = configName: "user-tmpfiles.d/home-manager-${configName}.conf"; + + mkConfigFile = + name: rules: + let + escapeArgument = lib.strings.escapeC [ + "\t" + "\n" + "\r" + " " + "\\" + ]; + mkRule = path: rule: '' + '${rule.type}' '${path}' '${rule.mode}' '${rule.user}' '${rule.group}' '${rule.age}' ${escapeArgument rule.argument} + ''; + in + { + text = '' + # This file was generated by Home Manager and should not be modified. + # Please change the option '${lib.showAttrPath (modulePrefix ++ [ name ])}' instead. + ${lib.pipe rules [ + (lib.mapAttrs (_path: lib.attrValues)) + (lib.mapAttrsToList (path: map (mkRule path))) + lib.flatten + lib.concatStrings + ]} + ''; + onChange = '' + run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${mkFileName name}' + ''; + }; + in { meta.maintainers = [ lib.maintainers.dawidsowa ]; - options.systemd.user.tmpfiles.rules = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "L /home/user/Documents - - - - /mnt/data/Documents" ]; + imports = [ + (lib.mkRemovedOptionModule [ "systemd" "user" "tmpfiles" "rules" ] '' + It has been replaced by 'systemd.user.tmpfiles.settings'. + '') + ]; + + options.systemd.user.tmpfiles.settings = lib.mkOption { description = '' - Rules for creating and cleaning up temporary files - automatically. See - {manpage}`tmpfiles.d(5)` - for the exact format. + Declare systemd-tmpfiles rules to create, delete, and clean up volatile + and temporary files and directories. + + Even though the service is called `*tmp*files` you can also create + persistent files. ''; + example = { + cache."%C".d = { + mode = "0755"; + user = "alice"; + group = "alice"; + age = "4 weeks"; + }; + }; + default = { }; + type = + let + attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; }; + nonEmptyAttrsWith' = + placeholder: elemType: + let + attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { }); + in + attrs + // { + name = "nonEmptyAttrsOf"; + description = "non-empty ${attrs.description}"; + emptyValue = { }; # no .value attribute, meaning there is not empty value + substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m); + }; + in + attrsWith' "config-name" ( + nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule) + ); }; - config = lib.mkIf (cfg.rules != [ ]) { + config = lib.mkIf (cfg.settings != { }) { assertions = [ (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) ]; + warnings = lib.flatten ( + lib.mapAttrsToListRecursive ( + path: value: + lib.optional + (lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null) + '' + The '${lib.showAttrPath (modulePrefix ++ path)}' option + appears to contain escape sequences, which will be escaped again. + Unescape them if this is not intended. The assigned string is: + "${value}" + '' + ) cfg.settings + ); + xdg.configFile = { - "user-tmpfiles.d/home-manager.conf" = { - text = '' - # This file is created automatically and should not be modified. - # Please change the option ‘systemd.user.tmpfiles.rules’ instead. - ${lib.concatStringsSep "\n" cfg.rules} - ''; - onChange = '' - run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} - ''; - }; "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "systemd/user/systemd-tmpfiles-setup.service".source = @@ -49,6 +204,9 @@ in "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; "systemd/user/systemd-tmpfiles-clean.service".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; - }; + } + // lib.mapAttrs' ( + name: rules: lib.nameValuePair (mkFileName name) (mkConfigFile name rules) + ) cfg.settings; }; } diff --git a/modules/programs/glab.nix b/modules/programs/glab.nix index 525456dc4..1aac3c004 100644 --- a/modules/programs/glab.nix +++ b/modules/programs/glab.nix @@ -36,14 +36,12 @@ in # Use `systemd-tmpfiles` since glab requires its configuration file to have # mode 0600. - systemd.user.tmpfiles.rules = - let - target = "${config.xdg.configHome}/glab-cli/config.yml"; - in - lib.mkIf (cfg.settings != { }) [ - "C+ ${target} - - - - ${yaml.generate "glab-config" cfg.settings}" - "z ${target} 0600" - ]; + systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) { + "${config.xdg.configHome}/glab-cli/config.yml" = { + "C+".argument = yaml.generate "glab-config" cfg.settings; + z.mode = "0600"; + }; + }; xdg.configFile."glab-cli/aliases.yml" = lib.mkIf (cfg.aliases != { }) { source = yaml.generate "glab-aliases" cfg.aliases; diff --git a/tests/default.nix b/tests/default.nix index 88ff40ece..735e1f246 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -203,6 +203,7 @@ import nmtSrc { ./modules/misc/numlock ./modules/misc/pam ./modules/misc/qt + ./modules/misc/tmpfiles ./modules/misc/xdg/linux.nix ./modules/misc/xsession ./modules/systemd diff --git a/tests/integration/standalone/rclone/sops-nix.nix b/tests/integration/standalone/rclone/sops-nix.nix index 71283edf5..a7baf0614 100644 --- a/tests/integration/standalone/rclone/sops-nix.nix +++ b/tests/integration/standalone/rclone/sops-nix.nix @@ -28,9 +28,12 @@ in uid = 1000; }; - systemd.tmpfiles.rules = [ - "f /home/alice/age-key 400 alice users - ${ageKey}" - ]; + systemd.tmpfiles.settings.age."/home/alice/age-key".f = { + mode = "400"; + user = "alice"; + group = "users"; + argument = ageKey; + }; home-manager.users.alice = { config, ... }: diff --git a/tests/modules/misc/tmpfiles/basic-rules.nix b/tests/modules/misc/tmpfiles/basic-rules.nix new file mode 100644 index 000000000..30a451737 --- /dev/null +++ b/tests/modules/misc/tmpfiles/basic-rules.nix @@ -0,0 +1,24 @@ +{ + imports = [ ./common-stubs.nix ]; + + systemd.user.tmpfiles.settings = { + cache."%C".d.age = "4 weeks"; + myTool."%h/.config/myTool.conf"."f+" = { + mode = "0644"; + user = "alice"; + group = "users"; + argument = "my unescaped config"; + }; + }; + + nmt.script = '' + cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf + assertFileExists $cacheRulesFile + assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $" + + myToolRulesFile=home-files/.config/user-tmpfiles.d/home-manager-myTool.conf + assertFileExists $myToolRulesFile + assertFileRegex $myToolRulesFile \ + "^'f+' '%h/.config/myTool.conf' '0644' 'alice' 'users' '-' my\\\\x20unescaped\\\\x20config$" + ''; +} diff --git a/tests/modules/misc/tmpfiles/common-stubs.nix b/tests/modules/misc/tmpfiles/common-stubs.nix new file mode 100644 index 000000000..abc4c732d --- /dev/null +++ b/tests/modules/misc/tmpfiles/common-stubs.nix @@ -0,0 +1,3 @@ +{ + test.stubs.systemd.outPath = null; +} diff --git a/tests/modules/misc/tmpfiles/default.nix b/tests/modules/misc/tmpfiles/default.nix new file mode 100644 index 000000000..1caa5fc7e --- /dev/null +++ b/tests/modules/misc/tmpfiles/default.nix @@ -0,0 +1,6 @@ +{ + tmpfiles-no-rules = ./no-rules.nix; + tmpfiles-basic-rules = ./basic-rules.nix; + + tmpfiles-escaped-argument-warning = ./escaped-argument-warning.nix; +} diff --git a/tests/modules/misc/tmpfiles/escaped-argument-warning.nix b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix new file mode 100644 index 000000000..9284de3c8 --- /dev/null +++ b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix @@ -0,0 +1,14 @@ +{ + imports = [ ./common-stubs.nix ]; + + systemd.user.tmpfiles.settings.foo.path.f.argument = "my\\x20unescaped\\x20config"; + + test.asserts.warnings.expected = [ + '' + The 'systemd.user.tmpfiles.settings.foo.path.f.argument' option + appears to contain escape sequences, which will be escaped again. + Unescape them if this is not intended. The assigned string is: + "my\x20unescaped\x20config" + '' + ]; +} diff --git a/tests/modules/misc/tmpfiles/no-rules.nix b/tests/modules/misc/tmpfiles/no-rules.nix new file mode 100644 index 000000000..8c8f0e77e --- /dev/null +++ b/tests/modules/misc/tmpfiles/no-rules.nix @@ -0,0 +1,9 @@ +{ + imports = [ ./common-stubs.nix ]; + + systemd.user.tmpfiles.settings = { }; + + nmt.script = '' + assertPathNotExists home-files/.config/user-tmpfiles.d/ + ''; +} From b4350d54c2ec735bc841eb0f583eb889c8159fe9 Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Fri, 31 Oct 2025 14:29:48 +0100 Subject: [PATCH 22/47] tmpfiles: add option to purge rules' targets on change --- modules/misc/tmpfiles.nix | 166 ++++++++++++------ modules/programs/glab.nix | 2 +- .../standalone/rclone/sops-nix.nix | 2 +- tests/modules/misc/tmpfiles/basic-rules.nix | 6 +- tests/modules/misc/tmpfiles/default.nix | 1 + .../tmpfiles/escaped-argument-warning.nix | 4 +- .../misc/tmpfiles/rules-with-purging.nix | 28 +++ 7 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 tests/modules/misc/tmpfiles/rules-with-purging.nix diff --git a/modules/misc/tmpfiles.nix b/modules/misc/tmpfiles.nix index 82b791d89..9403f7d85 100644 --- a/modules/misc/tmpfiles.nix +++ b/modules/misc/tmpfiles.nix @@ -89,6 +89,46 @@ let } ); + attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; }; + + nonEmptyAttrsWith' = + placeholder: elemType: + let + attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { }); + in + attrs + // { + name = "nonEmptyAttrsOf"; + description = "non-empty ${attrs.description}"; + emptyValue = { }; # no .value attribute, meaning there is not empty value + substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m); + }; + + configSubmodule = lib.types.submodule { + options.rules = lib.mkOption { + description = "The rules contained in this configuration."; + example = { + "%C".d = { + mode = "0755"; + user = "alice"; + group = "alice"; + age = "4 weeks"; + }; + }; + type = nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule); + }; + options.purgeOnChange = lib.mkOption { + description = '' + Whether the rules that are marked for purging, will automatically + be purged when the set of rules changes. + + See {manpage}`systemd-tmpfiles(8)` for details about purging. + ''; + type = lib.types.bool; + default = false; + }; + }; + modulePrefix = [ "systemd" "user" @@ -100,6 +140,9 @@ let mkConfigFile = name: rules: + { + suffix ? [ name ], + }: let escapeArgument = lib.strings.escapeC [ "\t" @@ -115,7 +158,7 @@ let { text = '' # This file was generated by Home Manager and should not be modified. - # Please change the option '${lib.showAttrPath (modulePrefix ++ [ name ])}' instead. + # Please change the option '${lib.showAttrPath (modulePrefix ++ suffix)}' instead. ${lib.pipe rules [ (lib.mapAttrs (_path: lib.attrValues)) (lib.mapAttrsToList (path: map (mkRule path))) @@ -128,6 +171,13 @@ let ''; }; + nonPurgedConfigs = lib.filterAttrs (_name: config: !config.purgeOnChange) cfg.settings; + purgedConfigs = lib.filterAttrs (_name: config: config.purgeOnChange) cfg.settings; + + # WARNING: When changing this path, the next home-manager generation will + # not find and the rules of the old generation that are subject to purging. + purgedRulesConfigName = "purge-on-change"; + in { meta.maintainers = [ lib.maintainers.dawidsowa ]; @@ -147,7 +197,7 @@ in persistent files. ''; example = { - cache."%C".d = { + cache.rules."%C".d = { mode = "0755"; user = "alice"; group = "alice"; @@ -155,58 +205,70 @@ in }; }; default = { }; - type = - let - attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; }; - nonEmptyAttrsWith' = - placeholder: elemType: - let - attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { }); - in - attrs - // { - name = "nonEmptyAttrsOf"; - description = "non-empty ${attrs.description}"; - emptyValue = { }; # no .value attribute, meaning there is not empty value - substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m); - }; - in - attrsWith' "config-name" ( - nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule) + type = attrsWith' "config-name" configSubmodule; + }; + + config = lib.mkMerge [ + + (lib.mkIf pkgs.stdenv.hostPlatform.isLinux { + # The activation script must be enabled unconditionally in order to + # guarantee that the old rules are purged even if the new set of rules + # is empty, i.e. `cfg.rulesToPurgeOnChange == [ ]`. + home.activation.purgeTmpfiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ( + let + relativeXdgConfigHome = lib.strings.removePrefix "${config.home.homeDirectory}/" config.xdg.configHome; + configPath = "home-files/${relativeXdgConfigHome}/${purgedRulesConfigName}"; + in + '' + if [[ -v oldGenPath && -f $oldGenPath/${configPath} ]] && + diff -q $oldGenPath/${configPath} $newGenPath/${configPath} &>/dev/null; then + verboseEcho "Purge old tmpfiles" + run ${pkgs.systemd}/bin/systemd-tmpfiles --user --purge ''${DRY_RUN:+--dry-run} $oldGenPath/${configPath} + fi + '' ); - }; + }) - config = lib.mkIf (cfg.settings != { }) { - assertions = [ - (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) - ]; + (lib.mkIf (cfg.settings != { }) { + assertions = [ + (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) + ]; - warnings = lib.flatten ( - lib.mapAttrsToListRecursive ( - path: value: - lib.optional - (lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null) - '' - The '${lib.showAttrPath (modulePrefix ++ path)}' option - appears to contain escape sequences, which will be escaped again. - Unescape them if this is not intended. The assigned string is: - "${value}" - '' - ) cfg.settings - ); + warnings = lib.flatten ( + lib.mapAttrsToListRecursive ( + path: value: + lib.optional + (lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null) + '' + The '${lib.showAttrPath (modulePrefix ++ path)}' option + appears to contain escape sequences, which will be escaped again. + Unescape them if this is not intended. The assigned string is: + "${value}" + '' + ) cfg.settings + ); - xdg.configFile = { - "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = - "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; - "systemd/user/systemd-tmpfiles-setup.service".source = - "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; - "systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source = - "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; - "systemd/user/systemd-tmpfiles-clean.service".source = - "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; - } - // lib.mapAttrs' ( - name: rules: lib.nameValuePair (mkFileName name) (mkConfigFile name rules) - ) cfg.settings; - }; + xdg.configFile = { + "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; + "systemd/user/systemd-tmpfiles-setup.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; + "systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; + "systemd/user/systemd-tmpfiles-clean.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; + } + // lib.mapAttrs' ( + name: config: lib.nameValuePair (mkFileName name) (mkConfigFile name config.rules { }) + ) nonPurgedConfigs + // lib.optionalAttrs (purgedConfigs != { }) { + ${mkFileName purgedRulesConfigName} = + let + purgedConfigsMerged = lib.foldl' lib.recursiveUpdate { } (lib.attrValues purgedConfigs); + in + mkConfigFile purgedRulesConfigName purgedConfigsMerged.rules { suffix = [ ]; }; + }; + }) + + ]; } diff --git a/modules/programs/glab.nix b/modules/programs/glab.nix index 1aac3c004..16ff2a86f 100644 --- a/modules/programs/glab.nix +++ b/modules/programs/glab.nix @@ -37,7 +37,7 @@ in # Use `systemd-tmpfiles` since glab requires its configuration file to have # mode 0600. systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) { - "${config.xdg.configHome}/glab-cli/config.yml" = { + rules."${config.xdg.configHome}/glab-cli/config.yml" = { "C+".argument = yaml.generate "glab-config" cfg.settings; z.mode = "0600"; }; diff --git a/tests/integration/standalone/rclone/sops-nix.nix b/tests/integration/standalone/rclone/sops-nix.nix index a7baf0614..fc676505e 100644 --- a/tests/integration/standalone/rclone/sops-nix.nix +++ b/tests/integration/standalone/rclone/sops-nix.nix @@ -28,7 +28,7 @@ in uid = 1000; }; - systemd.tmpfiles.settings.age."/home/alice/age-key".f = { + systemd.tmpfiles.settings.age.rules."/home/alice/age-key".f = { mode = "400"; user = "alice"; group = "users"; diff --git a/tests/modules/misc/tmpfiles/basic-rules.nix b/tests/modules/misc/tmpfiles/basic-rules.nix index 30a451737..68b00f87b 100644 --- a/tests/modules/misc/tmpfiles/basic-rules.nix +++ b/tests/modules/misc/tmpfiles/basic-rules.nix @@ -2,8 +2,8 @@ imports = [ ./common-stubs.nix ]; systemd.user.tmpfiles.settings = { - cache."%C".d.age = "4 weeks"; - myTool."%h/.config/myTool.conf"."f+" = { + cache.rules."%C".d.age = "4 weeks"; + myTool.rules."%h/.config/myTool.conf"."f+" = { mode = "0644"; user = "alice"; group = "users"; @@ -12,6 +12,8 @@ }; nmt.script = '' + assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf + cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf assertFileExists $cacheRulesFile assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $" diff --git a/tests/modules/misc/tmpfiles/default.nix b/tests/modules/misc/tmpfiles/default.nix index 1caa5fc7e..608796fe9 100644 --- a/tests/modules/misc/tmpfiles/default.nix +++ b/tests/modules/misc/tmpfiles/default.nix @@ -1,6 +1,7 @@ { tmpfiles-no-rules = ./no-rules.nix; tmpfiles-basic-rules = ./basic-rules.nix; + tmpfiles-rules-with-purging = ./rules-with-purging.nix; tmpfiles-escaped-argument-warning = ./escaped-argument-warning.nix; } diff --git a/tests/modules/misc/tmpfiles/escaped-argument-warning.nix b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix index 9284de3c8..0a7441b7e 100644 --- a/tests/modules/misc/tmpfiles/escaped-argument-warning.nix +++ b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix @@ -1,11 +1,11 @@ { imports = [ ./common-stubs.nix ]; - systemd.user.tmpfiles.settings.foo.path.f.argument = "my\\x20unescaped\\x20config"; + systemd.user.tmpfiles.settings.foo.rules.path.f.argument = "my\\x20unescaped\\x20config"; test.asserts.warnings.expected = [ '' - The 'systemd.user.tmpfiles.settings.foo.path.f.argument' option + The 'systemd.user.tmpfiles.settings.foo.rules.path.f.argument' option appears to contain escape sequences, which will be escaped again. Unescape them if this is not intended. The assigned string is: "my\x20unescaped\x20config" diff --git a/tests/modules/misc/tmpfiles/rules-with-purging.nix b/tests/modules/misc/tmpfiles/rules-with-purging.nix new file mode 100644 index 000000000..a453bfcb6 --- /dev/null +++ b/tests/modules/misc/tmpfiles/rules-with-purging.nix @@ -0,0 +1,28 @@ +{ + imports = [ ./common-stubs.nix ]; + + systemd.user.tmpfiles.settings = { + cache.rules."%C".d.age = "4 weeks"; + myTool = { + rules = { + "%h/.config/myTool.conf"."f+".argument = "my_config"; + "%h/.config/myToolPurged.conf"."f+$".argument = "my_config_purged"; + }; + purgeOnChange = true; + }; + }; + + nmt.script = '' + cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf + assertFileExists $cacheRulesFile + assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $" + + assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-myTool.conf + myToolRulesFile=home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf + assertFileExists $myToolRulesFile + assertFileRegex $myToolRulesFile \ + "^'f+' '%h/.config/myTool.conf' '-' '-' '-' '-' my_config$" + assertFileRegex $myToolRulesFile \ + "^'f+$' '%h/.config/myToolPurged.conf' '-' '-' '-' '-' my_config_purged$" + ''; +} From 7503ffb0b00243bfd087f0851403291ec5b76db0 Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Fri, 31 Oct 2025 15:07:20 +0100 Subject: [PATCH 23/47] tmpfiles: add maintainer bmrips --- modules/misc/tmpfiles.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/misc/tmpfiles.nix b/modules/misc/tmpfiles.nix index 9403f7d85..87b87c937 100644 --- a/modules/misc/tmpfiles.nix +++ b/modules/misc/tmpfiles.nix @@ -180,7 +180,10 @@ let in { - meta.maintainers = [ lib.maintainers.dawidsowa ]; + meta.maintainers = with lib.maintainers; [ + bmrips + dawidsowa + ]; imports = [ (lib.mkRemovedOptionModule [ "systemd" "user" "tmpfiles" "rules" ] '' From b5ed4afc2277339bdf0e9edf59befff7350cf075 Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Fri, 31 Oct 2025 15:06:41 +0100 Subject: [PATCH 24/47] glab: remove the config file if it is empty or glab disabled --- modules/programs/glab.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/programs/glab.nix b/modules/programs/glab.nix index 16ff2a86f..75f8c4eaf 100644 --- a/modules/programs/glab.nix +++ b/modules/programs/glab.nix @@ -38,7 +38,7 @@ in # mode 0600. systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) { rules."${config.xdg.configHome}/glab-cli/config.yml" = { - "C+".argument = yaml.generate "glab-config" cfg.settings; + "C+$".argument = yaml.generate "glab-config" cfg.settings; z.mode = "0600"; }; }; From 2318e30ea10b0b65b00c053e9ef432f27a215e3c Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Mon, 3 Nov 2025 12:11:23 +0100 Subject: [PATCH 25/47] tests: `hostPlatform` -> `stdenv.hostPlatform` --- tests/modules/programs/firefox/profiles/overwrite/default.nix | 2 +- tests/modules/programs/firefox/profiles/settings/default.nix | 2 +- tests/modules/programs/firefox/profiles/userchrome/default.nix | 2 +- tests/modules/programs/firefox/state-version-19_09.nix | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/modules/programs/firefox/profiles/overwrite/default.nix b/tests/modules/programs/firefox/profiles/overwrite/default.nix index 54014bea2..1dcfa35f9 100644 --- a/tests/modules/programs/firefox/profiles/overwrite/default.nix +++ b/tests/modules/programs/firefox/profiles/overwrite/default.nix @@ -39,7 +39,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/profiles/settings/default.nix b/tests/modules/programs/firefox/profiles/settings/default.nix index 9aa8af6ac..e48fa14f1 100644 --- a/tests/modules/programs/firefox/profiles/settings/default.nix +++ b/tests/modules/programs/firefox/profiles/settings/default.nix @@ -39,7 +39,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/profiles/userchrome/default.nix b/tests/modules/programs/firefox/profiles/userchrome/default.nix index 17ff8ddc8..f1b01c642 100644 --- a/tests/modules/programs/firefox/profiles/userchrome/default.nix +++ b/tests/modules/programs/firefox/profiles/userchrome/default.nix @@ -37,7 +37,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/state-version-19_09.nix b/tests/modules/programs/firefox/state-version-19_09.nix index cabb8c284..3da8f1374 100644 --- a/tests/modules/programs/firefox/state-version-19_09.nix +++ b/tests/modules/programs/firefox/state-version-19_09.nix @@ -13,7 +13,7 @@ in { imports = [ firefoxMockOverlay ]; - config = lib.mkIf (config.test.enableBig && !pkgs.hostPlatform.isDarwin) ( + config = lib.mkIf (config.test.enableBig && !pkgs.stdenv.hostPlatform.isDarwin) ( { home.stateVersion = "19.09"; } From 8c824254b1ed9e797f6235fc3c62f365893c561a Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Mon, 3 Nov 2025 10:55:43 +0100 Subject: [PATCH 26/47] glab: coerce glab tmpfile rule argument to string --- modules/programs/glab.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/programs/glab.nix b/modules/programs/glab.nix index 75f8c4eaf..84d1817ea 100644 --- a/modules/programs/glab.nix +++ b/modules/programs/glab.nix @@ -38,7 +38,7 @@ in # mode 0600. systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) { rules."${config.xdg.configHome}/glab-cli/config.yml" = { - "C+$".argument = yaml.generate "glab-config" cfg.settings; + "C+$".argument = "${yaml.generate "glab-config" cfg.settings}"; z.mode = "0600"; }; }; From c93684cd8717be1fb704df9ba2746572a0fed2bf Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Mon, 3 Nov 2025 11:54:11 +0100 Subject: [PATCH 27/47] tmpfiles: use correct path in the `onChange` hook --- modules/misc/tmpfiles.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/misc/tmpfiles.nix b/modules/misc/tmpfiles.nix index 87b87c937..531e3cd87 100644 --- a/modules/misc/tmpfiles.nix +++ b/modules/misc/tmpfiles.nix @@ -167,7 +167,7 @@ let ]} ''; onChange = '' - run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${mkFileName name}' + run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${config.xdg.configHome}/${mkFileName name}' ''; }; From 64c49b1aa537a1420f8bc8ff6ab47e5ba2c75208 Mon Sep 17 00:00:00 2001 From: bruce oberg Date: Mon, 29 Sep 2025 16:00:20 -0700 Subject: [PATCH 28/47] home-manager: fix option subcommand - calls nix-instatiate instead of nixos-option (using nix-option's underlying nix script). - loops over options to display since nixos-option can only process a single option. - passes through the --recursive flag from nixos-option. and includes --help and man page for the flag. details: nixos-option was changed from a C++ command to a shell script that feeds a nix script (with arguments) to nix-instatiate. in the process, the --config_expr and --options_expr we once passed to nixos-option were removed. without changing the nixos-option shell script, we have no may to override the arguments to the nixos-option nix script. luckily, we can use our modulesExpr as a direct argument to the new nixos-option nix script. unluckily, the nix script does not accept multiple options per instantiation. so we are also looping through the given options ourselves and feeding them each to nixos-option's nix script. the nixos-option shell and nix scripts are in different places in the nix store, so we have to search the store for the nix script given the location of the shell script. also, the nixos-option nix script wants a 'recursive' flag, so we now honor that flag for the home-manager option subcommand. --- docs/home-manager.1 | 8 +++-- home-manager/home-manager | 61 ++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/docs/home-manager.1 b/docs/home-manager.1 index 068a41aad..3cd386369 100644 --- a/docs/home-manager.1 +++ b/docs/home-manager.1 @@ -24,7 +24,7 @@ .Cm | generations .Cm | help .Cm | news -.Cm | option Ar option.name +.Cm | option Oo Fl -recursive Oc Ar option.name .Cm | packages .Cm | remove-generations Ar ID \&... .Cm | switch @@ -138,10 +138,14 @@ Show news entries in a pager. .RE .PP -.It Cm option Ar option.name +.It Cm option Oo Fl -recursive Oc Ar option.name .RS 4 Inspect the given option name in the home configuration, like \fBnixos-option\fR(8)\&. +.sp +If the +.Fl -recursive +option is given, print all the values at or below the option name recursively\&. .RE .Pp diff --git a/home-manager/home-manager b/home-manager/home-manager index e72151d28..7b4bcf490 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -232,7 +232,29 @@ function doInspectOption() { fi setConfigFile - local extraArgs=("$@") + local paths=() + local recursive=false + + while (( $# > 0 )); do + local opt="$1" + shift + + case $opt in + --recursive) + recursive=true;; + *) + # Remove trailing dot if exists, match the behavior of + # old nixos-option and make shell completions happy + paths+=("${opt%.}") + ;; + esac + done + + if [[ ${#paths[@]} -eq 0 ]]; then + paths=("") + fi + + local extraArgs=() for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs=("${extraArgs[@]}" "-I" "$p") @@ -256,11 +278,24 @@ function doInspectOption() { modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};" modulesExpr+=" pkgs = import {}; check = true; })" - nixos-option \ - --options_expr "$modulesExpr.options" \ - --config_expr "$modulesExpr.config" \ - "${extraArgs[@]}" \ - "${PASSTHROUGH_OPTS[@]}" + local NIXOS_OPTION_CMD NIXOS_OPTION_REAL NIXOS_OPTION_DIR NIXOS_OPTION_NIX + NIXOS_OPTION_CMD=$(command -v nixos-option) + NIXOS_OPTION_REAL=$(realpath "${NIXOS_OPTION_CMD}") + NIXOS_OPTION_NIX=$(nix-store -q --references "${NIXOS_OPTION_REAL}" | grep 'nixos-option\.nix$') + + if [[ ! -f "$NIXOS_OPTION_NIX" ]]; then + _iError "nixos-option.nix not found." + exit 1 + fi + + for path in "${paths[@]}"; do + nix-instantiate --eval --json \ + --arg nixos "$modulesExpr" \ + --argstr path "$path" \ + --arg recursive "$recursive" \ + "$NIXOS_OPTION_NIX" \ + | jq -r + done } function doInit() { @@ -1063,9 +1098,11 @@ function doHelp() { echo echo " edit Open the home configuration in \$VISUAL or \$EDITOR" echo - echo " option OPTION.NAME" + echo " option [--recursive] OPTION.NAME" echo " Inspect configuration option named OPTION.NAME." echo + echo " --recursive Print all the values at or below the option name recursively." + echo echo " build Build configuration into result directory" echo echo " init [--switch] [DIR]" @@ -1211,6 +1248,16 @@ while [[ $# -gt 0 ]]; do ;; esac ;; + --recursive) + case $COMMAND in + option) + COMMAND_ARGS+=("$opt") + ;; + *) + errTopLevelSubcommandOpt "--recursive" "option" + ;; + esac + ;; --option|--arg|--argstr) [[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt" [[ -v 2 ]] || errMissingOptArg "$opt $1" From 3c16ac3646c554b21676464f85dec450f109041a Mon Sep 17 00:00:00 2001 From: bruce oberg Date: Thu, 30 Oct 2025 16:31:50 -0700 Subject: [PATCH 29/47] home manager: add test for option subcommand - added a test to the standalone-basics unit: the option subcommand queries the `home.username` value and ensures that something sane is returned. details: - the option subcommand was broken by upstream changes to the `nixos-option` command. - hm had no tests for the option subcommand, so the problem was discovered by users. - output from nixos-option probably starts with 'Value:\n "alice"', but we're only looking for 'alice' because the output format is not guaranteed. --- tests/integration/standalone/standard-basics.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integration/standalone/standard-basics.nix b/tests/integration/standalone/standard-basics.nix index ed565e18c..1a47aad5e 100644 --- a/tests/integration/standalone/standard-basics.nix +++ b/tests/integration/standalone/standard-basics.nix @@ -107,6 +107,12 @@ assert expected in actual, \ f"expected generations to contain {expected}, but found {actual}" + with subtest("Home Manager option"): + actual = succeed_as_alice("home-manager option home.username") + expected = "alice" + assert expected in actual, \ + f"expected generations to contain {expected}, but found {actual}" + with subtest("Home Manager uninstallation"): succeed_as_alice("yes | home-manager uninstall -L") From e22fb25cdedbc1f68f68177c5fbeb06a0a31b17a Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 3 Nov 2025 00:28:16 +0700 Subject: [PATCH 30/47] rio: use stub package for all test cases --- tests/modules/programs/rio/empty-settings.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/modules/programs/rio/empty-settings.nix b/tests/modules/programs/rio/empty-settings.nix index e89374d1f..07fc296bd 100644 --- a/tests/modules/programs/rio/empty-settings.nix +++ b/tests/modules/programs/rio/empty-settings.nix @@ -1,5 +1,7 @@ +{ config, ... }: { programs.rio.enable = true; + programs.rio.package = config.lib.test.mkStubPackage { }; nmt.script = '' assertPathNotExists home-files/.config/rio From 9d6e28fd32bf855d3f734ce5f2e4e2e2fc32212e Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 3 Nov 2025 00:35:27 +0700 Subject: [PATCH 31/47] rio: add support for custom themes --- modules/programs/rio.nix | 29 ++++++++++++++++++ tests/modules/programs/rio/default.nix | 1 + tests/modules/programs/rio/example-themes.nix | 30 +++++++++++++++++++ tests/modules/programs/rio/foobar.toml | 4 +++ 4 files changed, 64 insertions(+) create mode 100644 tests/modules/programs/rio/example-themes.nix create mode 100644 tests/modules/programs/rio/foobar.toml diff --git a/modules/programs/rio.nix b/modules/programs/rio.nix index 491727f85..b4da2345f 100644 --- a/modules/programs/rio.nix +++ b/modules/programs/rio.nix @@ -28,6 +28,25 @@ in for options. ''; }; + + themes = lib.mkOption { + type = with lib.types; attrsOf (either settingsFormat.type path); + default = { }; + description = '' + Theme files written to {file}`$XDG_CONFIG_HOME/rio/themes/`. See + for + supported values. + ''; + example = lib.literalExpression '' + { + foobar.colors = { + background = "#282a36"; + green = "#50fa7b"; + dim-green = "#06572f"; + }; + } + ''; + }; }; meta.maintainers = [ lib.maintainers.otavio ]; @@ -42,6 +61,16 @@ in xdg.configFile."rio/config.toml".source = if lib.isPath cfg.settings then cfg.settings else settingsFormat.generate "rio.toml" cfg.settings; }) + + (lib.mkIf (cfg.themes != { }) { + xdg.configFile = lib.mapAttrs' ( + name: value: + lib.nameValuePair "rio/themes/${name}.toml" { + source = + if builtins.isPath value then value else settingsFormat.generate "rio-theme-${name}.toml" value; + } + ) cfg.themes; + }) ] ); } diff --git a/tests/modules/programs/rio/default.nix b/tests/modules/programs/rio/default.nix index a6aba7fe1..461c51605 100644 --- a/tests/modules/programs/rio/default.nix +++ b/tests/modules/programs/rio/default.nix @@ -1,4 +1,5 @@ { rio-example-settings = ./example-settings.nix; rio-empty-settings = ./empty-settings.nix; + rio-themes = ./example-themes.nix; } diff --git a/tests/modules/programs/rio/example-themes.nix b/tests/modules/programs/rio/example-themes.nix new file mode 100644 index 000000000..28a25d199 --- /dev/null +++ b/tests/modules/programs/rio/example-themes.nix @@ -0,0 +1,30 @@ +{ config, ... }: +{ + programs.rio = { + enable = true; + + package = config.lib.test.mkStubPackage { }; + + themes = { + foobar.colors = { + cyan = "#8be9fd"; + green = "#50fa7b"; + background = "#282a36"; + }; + + foobar2 = ./foobar.toml; + }; + }; + + nmt.script = '' + assertFileExists home-files/.config/rio/themes/foobar.toml + assertFileExists home-files/.config/rio/themes/foobar2.toml + + assertFileContent \ + home-files/.config/rio/themes/foobar.toml \ + ${./foobar.toml} + assertFileContent \ + home-files/.config/rio/themes/foobar2.toml \ + ${./foobar.toml} + ''; +} diff --git a/tests/modules/programs/rio/foobar.toml b/tests/modules/programs/rio/foobar.toml new file mode 100644 index 000000000..ab93034f8 --- /dev/null +++ b/tests/modules/programs/rio/foobar.toml @@ -0,0 +1,4 @@ +[colors] +background = "#282a36" +cyan = "#8be9fd" +green = "#50fa7b" From a5fee077929ae2f2800c3087dce5e1abb4edfbc6 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 3 Nov 2025 00:39:36 +0700 Subject: [PATCH 32/47] rio: reformat --- modules/programs/rio.nix | 67 ++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/modules/programs/rio.nix b/modules/programs/rio.nix index b4da2345f..b3a45f2d3 100644 --- a/modules/programs/rio.nix +++ b/modules/programs/rio.nix @@ -5,22 +5,34 @@ ... }: let + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + mkIf + mkMerge + types + literalExpression + mapAttrs' + nameValuePair + ; + cfg = config.programs.rio; settingsFormat = pkgs.formats.toml { }; in { options.programs.rio = { - enable = lib.mkEnableOption null // { + enable = mkEnableOption null // { description = '' Enable Rio, a terminal built to run everywhere, as a native desktop applications by Rust/WebGPU or even in the browsers powered by WebAssembly/WebGPU. ''; }; - package = lib.mkPackageOption pkgs "rio" { nullable = true; }; + package = mkPackageOption pkgs "rio" { nullable = true; }; - settings = lib.mkOption { + settings = mkOption { type = settingsFormat.type; default = { }; description = '' @@ -29,15 +41,15 @@ in ''; }; - themes = lib.mkOption { - type = with lib.types; attrsOf (either settingsFormat.type path); + themes = mkOption { + type = with types; attrsOf (either settingsFormat.type path); default = { }; description = '' Theme files written to {file}`$XDG_CONFIG_HOME/rio/themes/`. See for supported values. ''; - example = lib.literalExpression '' + example = literalExpression '' { foobar.colors = { background = "#282a36"; @@ -50,27 +62,28 @@ in }; meta.maintainers = [ lib.maintainers.otavio ]; - config = lib.mkIf cfg.enable ( - lib.mkMerge [ - { - home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; - } + config = mkIf cfg.enable (mkMerge [ + { + home.packages = mkIf (cfg.package != null) [ cfg.package ]; + } - # Only manage configuration if not empty - (lib.mkIf (cfg.settings != { }) { - xdg.configFile."rio/config.toml".source = - if lib.isPath cfg.settings then cfg.settings else settingsFormat.generate "rio.toml" cfg.settings; - }) + # Only manage configuration if not empty + (mkIf (cfg.settings != { }) { + xdg.configFile."rio/config.toml".source = + if builtins.isPath cfg.settings then + cfg.settings + else + settingsFormat.generate "rio.toml" cfg.settings; + }) - (lib.mkIf (cfg.themes != { }) { - xdg.configFile = lib.mapAttrs' ( - name: value: - lib.nameValuePair "rio/themes/${name}.toml" { - source = - if builtins.isPath value then value else settingsFormat.generate "rio-theme-${name}.toml" value; - } - ) cfg.themes; - }) - ] - ); + (mkIf (cfg.themes != { }) { + xdg.configFile = mapAttrs' ( + name: value: + nameValuePair "rio/themes/${name}.toml" { + source = + if builtins.isPath value then value else settingsFormat.generate "rio-theme-${name}.toml" value; + } + ) cfg.themes; + }) + ]); } From 95d65dddae7ae6300b22cf1d61cb5e71dca8da5b Mon Sep 17 00:00:00 2001 From: aldur Date: Mon, 3 Nov 2025 00:37:06 +0000 Subject: [PATCH 33/47] gpg: fix correctly setting trust for all keys When passing `gpg.publicKeys` a `source` including _multiple_ keys, only the first one in `source` will have `trust` set correctly. This commit fixes the issue and adds a corresponding test (failing without the patch, fixed with it). --- modules/programs/gpg.nix | 2 +- tests/modules/programs/gpg/default.nix | 1 + .../programs/gpg/multiple-keys-trust.nix | 61 +++++++++++++++++++ .../programs/gpg/test-keys/multiple-keys.asc | 45 ++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/modules/programs/gpg/multiple-keys-trust.nix create mode 100644 tests/modules/programs/gpg/test-keys/multiple-keys.asc diff --git a/modules/programs/gpg.nix b/modules/programs/gpg.nix index e3a7a8e87..1978a1b68 100644 --- a/modules/programs/gpg.nix +++ b/modules/programs/gpg.nix @@ -125,7 +125,7 @@ let function importTrust() { local keyIds trust - IFS='\n' read -ra keyIds <<< "$(gpgKeyId "$1")" + mapfile -t keyIds <<< "$(gpgKeyId "$1")" trust="$2" for id in "''${keyIds[@]}" ; do { echo trust; echo "$trust"; (( trust == 5 )) && echo y; echo quit; } \ diff --git a/tests/modules/programs/gpg/default.nix b/tests/modules/programs/gpg/default.nix index a3949b186..40ed739ec 100644 --- a/tests/modules/programs/gpg/default.nix +++ b/tests/modules/programs/gpg/default.nix @@ -1,5 +1,6 @@ { gpg-immutable-keyfiles = ./immutable-keyfiles.nix; gpg-mutable-keyfiles = ./mutable-keyfiles.nix; + gpg-multiple-keys-trust = ./multiple-keys-trust.nix; gpg-override-defaults = ./override-defaults.nix; } diff --git a/tests/modules/programs/gpg/multiple-keys-trust.nix b/tests/modules/programs/gpg/multiple-keys-trust.nix new file mode 100644 index 000000000..966443cf6 --- /dev/null +++ b/tests/modules/programs/gpg/multiple-keys-trust.nix @@ -0,0 +1,61 @@ +{ realPkgs, ... }: + +{ + programs.gpg = { + enable = true; + package = realPkgs.gnupg; + + mutableKeys = false; + mutableTrust = false; + + publicKeys = [ + { + # This file contains three public keys + # The bug causes only the first key to have trust set + source = ./test-keys/multiple-keys.asc; + trust = "ultimate"; # trust level 5 + } + ]; + }; + + nmt.script = '' + assertFileNotRegex activate "^export GNUPGHOME=/home/hm-user/.gnupg$" + + assertFileRegex activate \ + '^install -m 0700 /nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg "/home/hm-user/.gnupg/trustdb.gpg"$' + + # Setup GPGHOME + export GNUPGHOME=$(mktemp -d) + cp -r $TESTED/home-files/.gnupg/* $GNUPGHOME + TRUSTDB=$(grep -o '/nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg' $TESTED/activate) + install -m 0700 $TRUSTDB $GNUPGHOME/trustdb.gpg + + # Export Trust + export WORKDIR=$(mktemp -d) + ${realPkgs.gnupg}/bin/gpg -q --export-ownertrust > $WORKDIR/gpgtrust.txt + + echo "=== Trust database contents ===" + cat $WORKDIR/gpgtrust.txt + echo "=== End of trust database ===" + + # The test file contains three keys: + # - 13B06D9193E01E0F (Test User One) - fingerprint: B07502E7B7ED0A4AA3BF191913B06D9193E01E0F + # - 42E7B990011430DE (Test User Two) - fingerprint: 6A2A713AE7F93C8EA6D264B642E7B990011430DE + # - DFC825F8209CE742 (Test User Three) - fingerprint: E66D263DC7174345AB102829DFC825F8209CE742 + # + # All three keys should have ultimate trust (level 6 in ownertrust format) + # Due to the bug in importTrust function, only the first key gets trust set + + # Check that first key has ultimate trust (this works with current code) + assertFileRegex $WORKDIR/gpgtrust.txt \ + '^B07502E7B7ED0A4AA3BF191913B06D9193E01E0F:6:$' + + # Check that second key has ultimate trust (this FAILS due to bug) + assertFileRegex $WORKDIR/gpgtrust.txt \ + '^6A2A713AE7F93C8EA6D264B642E7B990011430DE:6:$' + + # Check that third key has ultimate trust (this FAILS due to bug) + assertFileRegex $WORKDIR/gpgtrust.txt \ + '^E66D263DC7174345AB102829DFC825F8209CE742:6:$' + ''; +} diff --git a/tests/modules/programs/gpg/test-keys/multiple-keys.asc b/tests/modules/programs/gpg/test-keys/multiple-keys.asc new file mode 100644 index 000000000..c8b9c6937 --- /dev/null +++ b/tests/modules/programs/gpg/test-keys/multiple-keys.asc @@ -0,0 +1,45 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGkHy/oBCADC4NT6P4eiOv1f9g8mhdLQlexO4Pefh33EicybD4tnlZZGVzYT +2J75slIGFV9+AOX/TXsws7+0IaZYB94a3p1NKoWeYh4XZy0HQ2HRJjNWeLQ41lFC +dCQ4A0JuqCurMFFdph59Xlh4ko3SXmPwNqXEmNX8LQlIDRNk+RiW+gJ4OC8DV6Do +YexeQHrHxtdGrStFmEygEAB5K1xqLRrzETvPubEmPEcrvhT/7W1+TwCb/haKo+Is +OgFcaJFv7CR6EbYh3DNZa4Zrd/WpNAL8+Kmz89VTdw0qaSYJxV9uR4DdmgX+2tAv +WmLuTuPMabU599p9nRUqk1Pj5fit6octCxX9ABEBAAG0IVRlc3QgVXNlciBPbmUg +PHRlc3QxQGV4YW1wbGUuY29tPokBTwQTAQoAORYhBLB1Aue37QpKo78ZGROwbZGT +4B4PBQJpB8v6AxsvBAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRATsG2Rk+Ae +D54fB/9EN7IjdwARheioFsZlifda5t31l084eYsq9kLzjCrxCXNlDZEIi6QrNBBA +CDZyv5bM+JLrZPbZ/1J1caoB6W9+ARPLiERWMhql7JNWSS/4Yhf/L0aD0C3pJFJf +h3bcSxhAzXBL3857cELR88UeV7NHPNdJsKVX0h7r1xe1D1oGZd19qbyZx3FJLzH8 +p01ZkLoKdKAh42x+XN6KrOWGWFyvLX56pXjp9mjero2iDpUlBdIV15CFJ+aoVI3B +KG26z4B7/L8kQVO2eH41k/i39u9SuvuCinYcNQ/5/blpaIc7xqL5jI1gapzE4bBu +GzGOKJoWRgGJDUZzyvTtxbI/nsK6mQENBGkHy/oBCADHGrIJ1uTGWJvSt+2pmqxK +ruXQvVxQva3GbYIgePQa88PzhORYTnuskEdOhNhMTaxKWbxS1bfDXf3Akjis+kHb +xLK692XtKFf88ALV6ts0Rd4YRG6BCcwMPAfFuQhyQRxclNk5XHzaH6IvKvmrSkvG +wilLkrdj9hW32FvVYDyjdiDSbvs05d8EfRr7UF/fMQC5HOJJ6VSC7HJ7tQGWvtNG +eyr/I61OSDxhf6PF5CfuepajO0nzsVHvsXTxoJwYbx+zXSlGxTsHWYxp6r0MdPE/ +vCNmvrfpz4PoTiE43Xa3XsYSO2gRCpMYJKQaxl5pCfBGSmKpCF1YDBSTrRYyacyv +ABEBAAG0IVRlc3QgVXNlciBUd28gPHRlc3QyQGV4YW1wbGUuY29tPokBTwQTAQoA +ORYhBGoqcTrn+TyOptJktkLnuZABFDDeBQJpB8v6AxsvBAULCQgHAgYVCgkICwIE +FgIDAQIeAQIXgAAKCRBC57mQARQw3nIGB/9/j1SIk+DxmCeT2fihQmS7lubDoq1I +FUdjb7cAGBs4KAmJh8MVMsYyB+EtaVC8qu4C5EgNNV0+c2H8UishGcZvMm9Qg7LQ +MTSGKLwXikaiIvyw3zlh1FpJn2rYUSvCplVswhF/dfSlenmU81eiPigYsvzVoa8h +xJNn01DLu4cd2VsBhWW/2w3DKSvVHRPdlPTPrqkjzMQRy2ULa2yTWiiuxWJxHuj0 +3ocvLGlpyyvIwyoFVG4Lex4r+jSL3RCllEUjADAMgDPfhoTEerfgORCVEqGE/JLR +MVrTl6bMuodGehXgCRalcg9ChUADBHS4fZ0NiH46QhTblwRRFc2K6WbzmQENBGkH +y/oBCADAzZTgBmulUSr29gmBELA1gpMNHZ3J/2R3mTXMFaZAsi84uCZNyLLrDhU4 +WaXVRURlwY4eHdvIMc3IM846s0SkLKDy3cIbusQK9NDVS/69LRyKNiZMjEbpODZl +fT5AtQUOL1jAIxy/wVEKzqih0so6mfNCwKFshWyi4p2+E8dFT8apTvhwJkdpptb6 +q8Q1ABx+NRE1iSK+lFUw7xD7lLDvUYcHn6glpEMIGjg3/BLF74nVYFe6rCuFKgNt +GHLk1ZjoldbQRmTxdaKkb6vmfPWjbQuZCdNAUT87ljnrpdl3YxRN2ujQ1tHrWkby +C+anhmkdoQnqQPpICaeLe6NwHpPVABEBAAG0I1Rlc3QgVXNlciBUaHJlZSA8dGVz +dDNAZXhhbXBsZS5jb20+iQFPBBMBCgA5FiEE5m0mPccXQ0WrECgp38gl+CCc50IF +AmkHy/oDGy8EBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEN/IJfggnOdC7qEH +/idAjYhb9QNnOOu7lPkgLnPVanLCE20uHoGLeDUNkz2+2VFmkTu9poHKp4P7tW4e +/wMyy6uv4X1kcp6XcwVALx2HRU/PKLy1kNQFEeDocA1fx0wloJTfGfJpbxXWPFUG +oTVx0V2BwjiGK1+MTZCJQ+aqS2mXPLMPRv0ZKw8CQOeGHRJCD3NBEiWxpi5wncFM +DFDnaKrTCgmndRIafdXU3B7L4zZkNwcXRylkxVFjl938W5czbqa0o2LLadd/trJZ +YN/21BNkS/QmrH1Kapcgj5GvJp8ky4OpccrCTxfWLmRVfxtdo/N2woNyK9xvjiwd +TYMaXvrf93dAboJrOmiAtPA= +=tjTO +-----END PGP PUBLIC KEY BLOCK----- From 1f34c2c855751c74b01df5068cf4dd64ea2c7d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20Landur=C3=A9?= Date: Fri, 31 Oct 2025 12:01:41 +0100 Subject: [PATCH 34/47] zed-editor: options to generate debug.json Add `mutableUserDebug` and `userDebug` options to generate `debug.json` file. The options are heavily inspired by `mutableUserTasks` and `userTasks` options implementation. Closes #8091 --- modules/programs/zed-editor.nix | 41 +++++++ .../programs/zed-editor/debug-empty.nix | 83 +++++++++++++++ .../programs/zed-editor/debug-immutable.nix | 58 ++++++++++ tests/modules/programs/zed-editor/debug.nix | 100 ++++++++++++++++++ tests/modules/programs/zed-editor/default.nix | 3 + 5 files changed, 285 insertions(+) create mode 100644 tests/modules/programs/zed-editor/debug-empty.nix create mode 100644 tests/modules/programs/zed-editor/debug-immutable.nix create mode 100644 tests/modules/programs/zed-editor/debug.nix diff --git a/modules/programs/zed-editor.nix b/modules/programs/zed-editor.nix index 5b6fc4ff3..d95f59e88 100644 --- a/modules/programs/zed-editor.nix +++ b/modules/programs/zed-editor.nix @@ -81,6 +81,15 @@ in ''; }; + mutableUserDebug = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether user debug configurations (debug.json) can be updated by zed. + ''; + }; + userSettings = mkOption { type = jsonFormat.type; default = { }; @@ -140,6 +149,27 @@ in ''; }; + userDebug = mkOption { + type = jsonFormat.type; + default = [ ]; + example = literalExpression '' + [ + { + label = "Go (Delve)"; + adapter = "Delve"; + program = "$ZED_FILE"; + request = "launch"; + mode = "debug"; + } + ] + ''; + description = '' + Configuration written to Zed's {file}`debug.json`. + + Global debug configurations for Zed's [Debugger](https://zed.dev/docs/debugger). + ''; + }; + extensions = mkOption { type = types.listOf types.str; default = [ ]; @@ -241,6 +271,14 @@ in (jsonFormat.generate "zed-user-tasks" cfg.userTasks) ); }) + (mkIf (cfg.mutableUserDebug && cfg.userDebug != [ ]) { + zedDebugActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] ( + impureConfigMerger "[]" + "$dynamic + $static | group_by(.label) | map(reduce .[] as $item ({}; . * $item))" + "${config.xdg.configHome}/zed/debug.json" + (jsonFormat.generate "zed-user-debug" cfg.userDebug) + ); + }) ]; xdg.configFile = mkMerge [ @@ -265,6 +303,9 @@ in (mkIf (!cfg.mutableUserTasks && cfg.userTasks != [ ]) { "zed/tasks.json".source = jsonFormat.generate "zed-user-tasks" cfg.userTasks; }) + (mkIf (!cfg.mutableUserDebug && cfg.userDebug != [ ]) { + "zed/debug.json".source = jsonFormat.generate "zed-user-debug" cfg.userDebug; + }) ]; assertions = [ diff --git a/tests/modules/programs/zed-editor/debug-empty.nix b/tests/modules/programs/zed-editor/debug-empty.nix new file mode 100644 index 000000000..eed8e7354 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug-empty.nix @@ -0,0 +1,83 @@ +{ + config, + lib, + pkgs, + ... +}: + +{ + programs.zed-editor = { + enable = true; + package = config.lib.test.mkStubPackage { }; + userDebug = [ + { + label = "PHP: Listen to Xdebug"; + adapter = "Xdebug"; + request = "launch"; + port = 9003; + } + { + label = "PHP: Debug this test"; + adapter = "Xdebug"; + request = "launch"; + program = "vendor/bin/phpunit"; + args = [ + "--filter" + "$ZED_SYMBOL" + ]; + } + ]; + }; + + home.homeDirectory = lib.mkForce "/@TMPDIR@/hm-user"; + + nmt.script = + let + preexistingDebug = builtins.toFile "preexisting.json" ""; + + expectedContent = builtins.toFile "expected.json" '' + [ + { + "adapter": "Xdebug", + "args": [ + "--filter", + "$ZED_SYMBOL" + ], + "label": "PHP: Debug this test", + "program": "vendor/bin/phpunit", + "request": "launch" + }, + { + "adapter": "Xdebug", + "label": "PHP: Listen to Xdebug", + "port": 9003, + "request": "launch" + } + ] + ''; + + debugPath = ".config/zed/debug.json"; + activationScript = pkgs.writeScript "activation" config.home.activation.zedDebugActivation.data; + in + '' + export HOME=$TMPDIR/hm-user + + # Simulate preexisting debug + mkdir -p $HOME/.config/zed + cat ${preexistingDebug} > $HOME/${debugPath} + + # Run the activation script + substitute ${activationScript} $TMPDIR/activate --subst-var TMPDIR + chmod +x $TMPDIR/activate + $TMPDIR/activate + + # Validate the merged debug + assertFileExists "$HOME/${debugPath}" + assertFileContent "$HOME/${debugPath}" "${expectedContent}" + + # Test idempotency + $TMPDIR/activate + assertFileExists "$HOME/${debugPath}" + assertFileContent "$HOME/${debugPath}" "${expectedContent}" + ''; +} diff --git a/tests/modules/programs/zed-editor/debug-immutable.nix b/tests/modules/programs/zed-editor/debug-immutable.nix new file mode 100644 index 000000000..f433d9128 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug-immutable.nix @@ -0,0 +1,58 @@ +# Test custom keymap functionality +{ config, ... }: + +{ + programs.zed-editor = { + enable = true; + package = config.lib.test.mkStubPackage { }; + mutableUserDebug = false; + userDebug = [ + { + label = "PHP: Listen to Xdebug"; + adapter = "Xdebug"; + request = "launch"; + port = 9003; + } + { + label = "PHP: Debug this test"; + adapter = "Xdebug"; + request = "launch"; + program = "vendor/bin/phpunit"; + args = [ + "--filter" + "$ZED_SYMBOL" + ]; + } + ]; + }; + + nmt.script = + let + expectedContent = builtins.toFile "expected.json" '' + [ + { + "adapter": "Xdebug", + "label": "PHP: Listen to Xdebug", + "port": 9003, + "request": "launch" + }, + { + "adapter": "Xdebug", + "args": [ + "--filter", + "$ZED_SYMBOL" + ], + "label": "PHP: Debug this test", + "program": "vendor/bin/phpunit", + "request": "launch" + } + ] + ''; + + settingsPath = ".config/zed/debug.json"; + in + '' + assertFileExists "home-files/${settingsPath}" + assertFileContent "home-files/${settingsPath}" "${expectedContent}" + ''; +} diff --git a/tests/modules/programs/zed-editor/debug.nix b/tests/modules/programs/zed-editor/debug.nix new file mode 100644 index 000000000..b49fb85d3 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug.nix @@ -0,0 +1,100 @@ +{ + config, + lib, + pkgs, + ... +}: + +{ + programs.zed-editor = { + enable = true; + package = config.lib.test.mkStubPackage { }; + userDebug = [ + { + label = "PHP: Listen to Xdebug"; + adapter = "Xdebug"; + request = "launch"; + port = 9003; + } + { + label = "PHP: Debug this test"; + adapter = "Xdebug"; + request = "launch"; + program = "vendor/bin/phpunit"; + args = [ + "--filter" + "$ZED_SYMBOL" + ]; + } + ]; + }; + + home.homeDirectory = lib.mkForce "/@TMPDIR@/hm-user"; + + nmt.script = + let + preexistingDebug = builtins.toFile "preexisting.json" '' + [ + { + "label": "Debug active Python file", + "adapter": "Debugpy", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + } + ] + ''; + + expectedContent = builtins.toFile "expected.json" '' + [ + { + "label": "Debug active Python file", + "adapter": "Debugpy", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "adapter": "Xdebug", + "args": [ + "--filter", + "$ZED_SYMBOL" + ], + "label": "PHP: Debug this test", + "program": "vendor/bin/phpunit", + "request": "launch" + }, + { + "adapter": "Xdebug", + "label": "PHP: Listen to Xdebug", + "port": 9003, + "request": "launch" + } + ] + ''; + + debugPath = ".config/zed/debug.json"; + activationScript = pkgs.writeScript "activation" config.home.activation.zedDebugActivation.data; + in + '' + export HOME=$TMPDIR/hm-user + + # Simulate preexisting debug + mkdir -p $HOME/.config/zed + cat ${preexistingDebug} > $HOME/${debugPath} + + # Run the activation script + substitute ${activationScript} $TMPDIR/activate --subst-var TMPDIR + chmod +x $TMPDIR/activate + $TMPDIR/activate + + # Validate the merged debug + assertFileExists "$HOME/${debugPath}" + assertFileContent "$HOME/${debugPath}" "${expectedContent}" + + # Test idempotency + $TMPDIR/activate + assertFileExists "$HOME/${debugPath}" + assertFileContent "$HOME/${debugPath}" "${expectedContent}" + ''; +} diff --git a/tests/modules/programs/zed-editor/default.nix b/tests/modules/programs/zed-editor/default.nix index 1029dba99..b7717320c 100644 --- a/tests/modules/programs/zed-editor/default.nix +++ b/tests/modules/programs/zed-editor/default.nix @@ -11,5 +11,8 @@ zed-tasks = ./tasks.nix; zed-tasks-immutable = ./tasks-immutable.nix; zed-tasks-empty = ./tasks-empty.nix; + zed-debug = ./debug.nix; + zed-debug-immutable = ./debug-immutable.nix; + zed-debug-empty = ./debug-empty.nix; zed-themes = ./themes; } From 083b20c1a01533efe877bfe7198e8ccd7dbf3546 Mon Sep 17 00:00:00 2001 From: Thierry Delafontaine Date: Mon, 27 Oct 2025 12:15:04 +0100 Subject: [PATCH 35/47] mcp: init module --- modules/programs/mcp.nix | 64 ++++++++++++++++++++ tests/modules/programs/mcp/default.nix | 4 ++ tests/modules/programs/mcp/empty-servers.nix | 9 +++ tests/modules/programs/mcp/mcp.json | 17 ++++++ tests/modules/programs/mcp/servers.nix | 25 ++++++++ 5 files changed, 119 insertions(+) create mode 100644 modules/programs/mcp.nix create mode 100644 tests/modules/programs/mcp/default.nix create mode 100644 tests/modules/programs/mcp/empty-servers.nix create mode 100644 tests/modules/programs/mcp/mcp.json create mode 100644 tests/modules/programs/mcp/servers.nix diff --git a/modules/programs/mcp.nix b/modules/programs/mcp.nix new file mode 100644 index 000000000..9fedbdf2e --- /dev/null +++ b/modules/programs/mcp.nix @@ -0,0 +1,64 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + literalExpression + mkEnableOption + mkIf + mkOption + ; + + cfg = config.programs.mcp; + + jsonFormat = pkgs.formats.json { }; +in +{ + meta.maintainers = with lib.maintainers; [ delafthi ]; + + options.programs.mcp = { + enable = mkEnableOption "mcp"; + + servers = mkOption { + inherit (jsonFormat) type; + default = { }; + example = literalExpression '' + { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + } + ''; + description = '' + MCP server configurations written to + {file}`XDG_CONFIG_HOME/.config/mcp/mcp.json` + ''; + }; + }; + + config = mkIf cfg.enable { + xdg.configFile = mkIf (cfg.servers != { }) ( + let + mcp-config = { + mcpServers = cfg.servers; + }; + in + { + "mcp/mcp.json".source = jsonFormat.generate "mcp.json" mcp-config; + } + ); + }; +} diff --git a/tests/modules/programs/mcp/default.nix b/tests/modules/programs/mcp/default.nix new file mode 100644 index 000000000..2a10a18b8 --- /dev/null +++ b/tests/modules/programs/mcp/default.nix @@ -0,0 +1,4 @@ +{ + mcp-servers = ./servers.nix; + mcp-empty-servers = ./empty-servers.nix; +} diff --git a/tests/modules/programs/mcp/empty-servers.nix b/tests/modules/programs/mcp/empty-servers.nix new file mode 100644 index 000000000..63c72c5c0 --- /dev/null +++ b/tests/modules/programs/mcp/empty-servers.nix @@ -0,0 +1,9 @@ +{ + programs.mcp = { + enable = true; + servers = { }; + }; + nmt.script = '' + assertPathNotExists home-files/.config/mcp/mcp.json + ''; +} diff --git a/tests/modules/programs/mcp/mcp.json b/tests/modules/programs/mcp/mcp.json new file mode 100644 index 000000000..9e2571d5e --- /dev/null +++ b/tests/modules/programs/mcp/mcp.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "context7": { + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "serverUrl": "https://mcp.context7.com/mcp" + }, + "everything": { + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ], + "command": "npx" + } + } +} diff --git a/tests/modules/programs/mcp/servers.nix b/tests/modules/programs/mcp/servers.nix new file mode 100644 index 000000000..a63e0f4ae --- /dev/null +++ b/tests/modules/programs/mcp/servers.nix @@ -0,0 +1,25 @@ +{ + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + serverUrl = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + }; + }; + nmt.script = '' + assertFileExists home-files/.config/mcp/mcp.json + assertFileContent home-files/.config/mcp/mcp.json \ + ${./mcp.json} + ''; +} From c7403518704e2487b4deaa910628531aa91a66ee Mon Sep 17 00:00:00 2001 From: Thierry Delafontaine Date: Mon, 27 Oct 2025 12:15:04 +0100 Subject: [PATCH 36/47] opencode: add mcp module integration --- modules/programs/opencode.nix | 67 ++++++++++++++++--- tests/modules/programs/opencode/default.nix | 2 + .../mcp-integration-with-override.json | 27 ++++++++ .../mcp-integration-with-override.nix | 48 +++++++++++++ .../programs/opencode/mcp-integration.json | 30 +++++++++ .../programs/opencode/mcp-integration.nix | 36 ++++++++++ 6 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 tests/modules/programs/opencode/mcp-integration-with-override.json create mode 100644 tests/modules/programs/opencode/mcp-integration-with-override.nix create mode 100644 tests/modules/programs/opencode/mcp-integration.json create mode 100644 tests/modules/programs/opencode/mcp-integration.nix diff --git a/modules/programs/opencode.nix b/modules/programs/opencode.nix index 538cff2a3..8e74ca872 100644 --- a/modules/programs/opencode.nix +++ b/modules/programs/opencode.nix @@ -16,6 +16,35 @@ let cfg = config.programs.opencode; jsonFormat = pkgs.formats.json { }; + + transformMcpServer = name: server: { + name = name; + value = { + enabled = !(server.disabled or false); + } + // ( + if server ? url then + { + type = "remote"; + url = server.url; + } + // (lib.optionalAttrs (server ? headers) { headers = server.headers; }) + else if server ? command then + { + type = "local"; + command = [ server.command ] ++ (server.args or [ ]); + } + // (lib.optionalAttrs (server ? env) { environment = server.env; }) + else + { } + ); + }; + + transformedMcpServers = + if cfg.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then + lib.listToAttrs (lib.mapAttrsToList transformMcpServer config.programs.mcp.servers) + else + { }; in { meta.maintainers = with lib.maintainers; [ delafthi ]; @@ -25,6 +54,20 @@ in package = mkPackageOption pkgs "opencode" { nullable = true; }; + enableMcpIntegration = mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to integrate the MCP servers config from + {option}`programs.mcp.servers` into + {option}`programs.opencode.settings.mcp`. + + Note: Settings defined in {option}`programs.mcp.servers` are merged + with {option}`programs.opencode.settings.mcp`, with OpenCode settings + taking precedence. + ''; + }; + settings = mkOption { inherit (jsonFormat) type; default = { }; @@ -147,7 +190,7 @@ in Custom themes for opencode. The attribute name becomes the theme filename, and the value is either: - An attribute set, that is converted to a json - - A path to a file conaining the content + - A path to a file containing the content Themes are stored in {file}`$XDG_CONFIG_HOME/opencode/themes/` directory. Set `programs.opencode.settings.theme` to enable the custom theme. See for the documentation. @@ -159,13 +202,21 @@ in home.packages = mkIf (cfg.package != null) [ cfg.package ]; xdg.configFile = { - "opencode/config.json" = mkIf (cfg.settings != { }) { - source = jsonFormat.generate "config.json" ( - { - "$schema" = "https://opencode.ai/config.json"; - } - // cfg.settings - ); + "opencode/config.json" = mkIf (cfg.settings != { } || transformedMcpServers != { }) { + source = + let + # Merge MCP servers: transformed servers + user settings, with user settings taking precedence + mergedMcpServers = transformedMcpServers // (cfg.settings.mcp or { }); + # Merge all settings + mergedSettings = + cfg.settings // (lib.optionalAttrs (mergedMcpServers != { }) { mcp = mergedMcpServers; }); + in + jsonFormat.generate "config.json" ( + { + "$schema" = "https://opencode.ai/config.json"; + } + // mergedSettings + ); }; "opencode/AGENTS.md" = ( diff --git a/tests/modules/programs/opencode/default.nix b/tests/modules/programs/opencode/default.nix index 62d780d5d..74bfc6a8b 100644 --- a/tests/modules/programs/opencode/default.nix +++ b/tests/modules/programs/opencode/default.nix @@ -11,4 +11,6 @@ opencode-mixed-content = ./mixed-content.nix; opencode-themes-inline = ./themes-inline.nix; opencode-themes-path = ./themes-path.nix; + opencode-mcp-integration = ./mcp-integration.nix; + opencode-mcp-integration-with-override = ./mcp-integration-with-override.nix; } diff --git a/tests/modules/programs/opencode/mcp-integration-with-override.json b/tests/modules/programs/opencode/mcp-integration-with-override.json new file mode 100644 index 000000000..46df26ad1 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration-with-override.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "remote", + "url": "https://mcp.context7.com/mcp" + }, + "custom-server": { + "enabled": true, + "type": "remote", + "url": "https://example.com" + }, + "everything": { + "command": [ + "custom-command" + ], + "enabled": false, + "type": "local" + } + }, + "model": "anthropic/claude-sonnet-4-20250514", + "theme": "opencode" +} diff --git a/tests/modules/programs/opencode/mcp-integration-with-override.nix b/tests/modules/programs/opencode/mcp-integration-with-override.nix new file mode 100644 index 000000000..c66133b83 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration-with-override.nix @@ -0,0 +1,48 @@ +{ + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + }; + }; + + programs.opencode = { + enable = true; + enableMcpIntegration = true; + settings = { + theme = "opencode"; + model = "anthropic/claude-sonnet-4-20250514"; + # User's custom MCP settings should override generated ones + mcp = { + everything = { + enabled = false; # Override to disable + command = [ "custom-command" ]; + type = "local"; + }; + custom-server = { + enabled = true; + type = "remote"; + url = "https://example.com"; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists home-files/.config/opencode/config.json + assertFileContent home-files/.config/opencode/config.json \ + ${./mcp-integration-with-override.json} + ''; +} diff --git a/tests/modules/programs/opencode/mcp-integration.json b/tests/modules/programs/opencode/mcp-integration.json new file mode 100644 index 000000000..ddd917af9 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "remote", + "url": "https://mcp.context7.com/mcp" + }, + "disabled-server": { + "command": [ + "echo", + "test" + ], + "enabled": false, + "type": "local" + }, + "everything": { + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-everything" + ], + "enabled": true, + "type": "local" + } + } +} diff --git a/tests/modules/programs/opencode/mcp-integration.nix b/tests/modules/programs/opencode/mcp-integration.nix new file mode 100644 index 000000000..f3d0f1808 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration.nix @@ -0,0 +1,36 @@ +{ + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + disabled-server = { + command = "echo"; + args = [ "test" ]; + disabled = true; + }; + }; + }; + + programs.opencode = { + enable = true; + enableMcpIntegration = true; + }; + + nmt.script = '' + assertFileExists home-files/.config/opencode/config.json + assertFileContent home-files/.config/opencode/config.json \ + ${./mcp-integration.json} + ''; +} From 9ff9a94fd484a7e4f7bd79091a6ed65c927e6a3d Mon Sep 17 00:00:00 2001 From: Thierry Delafontaine Date: Mon, 27 Oct 2025 12:15:04 +0100 Subject: [PATCH 37/47] vscode: add mcp module integration --- modules/programs/vscode/default.nix | 70 +++++++++++++++- tests/modules/programs/vscode/default.nix | 2 + .../vscode/mcp-integration-default.json | 26 ++++++ .../programs/vscode/mcp-integration-test.json | 7 ++ .../vscode/mcp-integration-with-override.json | 25 ++++++ .../vscode/mcp-integration-with-override.nix | 79 +++++++++++++++++++ .../programs/vscode/mcp-integration.nix | 73 +++++++++++++++++ 7 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 tests/modules/programs/vscode/mcp-integration-default.json create mode 100644 tests/modules/programs/vscode/mcp-integration-test.json create mode 100644 tests/modules/programs/vscode/mcp-integration-with-override.json create mode 100644 tests/modules/programs/vscode/mcp-integration-with-override.nix create mode 100644 tests/modules/programs/vscode/mcp-integration.nix diff --git a/modules/programs/vscode/default.nix b/modules/programs/vscode/default.nix index 4eba83784..5c89da6e5 100644 --- a/modules/programs/vscode/default.nix +++ b/modules/programs/vscode/default.nix @@ -114,6 +114,33 @@ let isPath = p: builtins.isPath p || lib.isStorePath p; + transformMcpServerForVscode = + name: server: + let + # Remove the disabled field from the server config + cleanServer = lib.filterAttrs (n: v: n != "disabled") server; + in + { + name = name; + value = { + enabled = !(server.disabled or false); + } + // ( + if server ? url then + { + type = "http"; + } + // cleanServer + else if server ? command then + { + type = "stdio"; + } + // cleanServer + else + { } + ); + }; + profileType = types.submodule { options = { userSettings = mkOption { @@ -154,6 +181,20 @@ let ''; }; + enableMcpIntegration = mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to integrate the MCP servers config from + {option}`programs.mcp.servers` into + {option}`programs.vscode.profiles..userMcp`. + + Note: Settings defined in {option}`programs.mcp.servers` are merged + with {option}`programs.vscode.profiles..userMcp`, with VSCode + settings taking precedence. + ''; + }; + userMcp = mkOption { type = types.either types.path jsonFormat.type; default = { }; @@ -459,10 +500,31 @@ in if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks; }) - (mkIf (v.userMcp != { }) { - "${mcpFilePath n}".source = - if isPath v.userMcp then v.userMcp else jsonFormat.generate "vscode-user-mcp" v.userMcp; - }) + (mkIf + ( + v.userMcp != { } + || (v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { }) + ) + { + "${mcpFilePath n}".source = + if isPath v.userMcp then + v.userMcp + else + let + transformedMcpServers = + if v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then + lib.listToAttrs (lib.mapAttrsToList transformMcpServerForVscode config.programs.mcp.servers) + else + { }; + # Merge MCP servers: transformed servers + user servers, with user servers taking precedence + mergedServers = transformedMcpServers // ((v.userMcp.servers or { })); + # Merge all MCP config + mergedMcpConfig = + v.userMcp // (lib.optionalAttrs (mergedServers != { }) { servers = mergedServers; }); + in + jsonFormat.generate "vscode-user-mcp" mergedMcpConfig; + } + ) (mkIf (v.keybindings != [ ]) { "${keybindingsFilePath n}".source = diff --git a/tests/modules/programs/vscode/default.nix b/tests/modules/programs/vscode/default.nix index 4db8b2c8a..a62521746 100644 --- a/tests/modules/programs/vscode/default.nix +++ b/tests/modules/programs/vscode/default.nix @@ -24,6 +24,8 @@ let keybindings = import ./keybindings.nix; tasks = import ./tasks.nix; mcp = import ./mcp.nix; + mcp-integration = import ./mcp-integration.nix; + mcp-integration-with-override = import ./mcp-integration-with-override.nix; update-checks = import ./update-checks.nix; snippets = import ./snippets.nix; }; diff --git a/tests/modules/programs/vscode/mcp-integration-default.json b/tests/modules/programs/vscode/mcp-integration-default.json new file mode 100644 index 000000000..66e3dfd42 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-default.json @@ -0,0 +1,26 @@ +{ + "servers": { + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "disabled-server": { + "command": "echo", + "enabled": false, + "type": "stdio" + }, + "everything": { + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ], + "command": "npx", + "enabled": true, + "type": "stdio" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-test.json b/tests/modules/programs/vscode/mcp-integration-test.json new file mode 100644 index 000000000..4f35aeaf1 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-test.json @@ -0,0 +1,7 @@ +{ + "servers": { + "Github": { + "url": "https://api.githubcopilot.com/mcp/" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.json b/tests/modules/programs/vscode/mcp-integration-with-override.json new file mode 100644 index 000000000..10772d0bf --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.json @@ -0,0 +1,25 @@ +{ + "servers": { + "CustomServer": { + "type": "http", + "url": "https://example.com/mcp" + }, + "context7": { + "enabled": true, + "headers": { + "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" + }, + "type": "http", + "url": "https://mcp.context7.com/mcp" + }, + "everything": { + "args": [ + "-y", + "@modelcontextprotocol/server-everything" + ], + "command": "custom-npx", + "enabled": false, + "type": "stdio" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.nix b/tests/modules/programs/vscode/mcp-integration-with-override.nix new file mode 100644 index 000000000..a1d7371f0 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.nix @@ -0,0 +1,79 @@ +package: + +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.programs.vscode; + willUseIfd = package.pname != "vscode"; + + mcpFilePath = + name: + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json" + else + ".config/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json"; + +in + +lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) { + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + }; + }; + + programs.vscode = { + enable = true; + inherit package; + profiles = { + default = { + enableMcpIntegration = true; + # User MCP settings should override generated ones + userMcp = { + servers = { + everything = { + command = "custom-npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + enabled = false; + type = "stdio"; + }; + CustomServer = { + type = "http"; + url = "https://example.com/mcp"; + }; + }; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists "home-files/${mcpFilePath "default"}" + assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-with-override.json} + ''; +} diff --git a/tests/modules/programs/vscode/mcp-integration.nix b/tests/modules/programs/vscode/mcp-integration.nix new file mode 100644 index 000000000..e40056747 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration.nix @@ -0,0 +1,73 @@ +package: + +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.programs.vscode; + willUseIfd = package.pname != "vscode"; + + mcpFilePath = + name: + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json" + else + ".config/${cfg.nameShort}/User/${ + lib.optionalString (name != "default") "profiles/${name}/" + }mcp.json"; + +in + +lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) { + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + disabled-server = { + command = "echo"; + disabled = true; + }; + }; + }; + + programs.vscode = { + enable = true; + inherit package; + profiles = { + default.enableMcpIntegration = true; + test.userMcp = { + servers = { + Github = { + url = "https://api.githubcopilot.com/mcp/"; + }; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists "home-files/${mcpFilePath "default"}" + assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-default.json} + + assertFileExists "home-files/${mcpFilePath "test"}" + assertFileContent "home-files/${mcpFilePath "test"}" ${./mcp-integration-test.json} + ''; +} From 1342b821db15b6c79731310ba787d152cd60e74b Mon Sep 17 00:00:00 2001 From: Thierry Delafontaine Date: Mon, 3 Nov 2025 11:25:16 +0100 Subject: [PATCH 38/47] news: add entry for mcp module and integrations Add news entry documenting the new `programs.mcp` module and MCP integration support in OpenCode and VSCode modules. --- .../misc/news/2025/11/2025-11-03_11-18-15.nix | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 modules/misc/news/2025/11/2025-11-03_11-18-15.nix diff --git a/modules/misc/news/2025/11/2025-11-03_11-18-15.nix b/modules/misc/news/2025/11/2025-11-03_11-18-15.nix new file mode 100644 index 000000000..82d9dd5bb --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_11-18-15.nix @@ -0,0 +1,25 @@ +{ + time = "2025-11-03T10:18:15+00:00"; + condition = true; + message = '' + A new module 'programs.mcp' is now available for managing Model + Context Protocol (MCP) server configurations. + + The 'programs.mcp.servers' option allows you to define MCP servers + in a central location. These configurations can be automatically + integrated into applications that support MCP. + + Two modules now support MCP integration: + + - 'programs.opencode.enableMcpIntegration': Integrates MCP servers + into OpenCode's configuration. + + - 'programs.vscode.profiles..enableMcpIntegration': Integrates + MCP servers into VSCode profiles. + + When integration is enabled, servers from 'programs.mcp.servers' are + merged with application-specific MCP settings, with the latter taking + precedence. This allows you to define MCP servers once and reuse them + across multiple applications. + ''; +} From 5cdf9ef99563a457f12a712e1f42c4f0d709203c Mon Sep 17 00:00:00 2001 From: leiserfg Date: Fri, 31 Oct 2025 13:16:27 +0100 Subject: [PATCH 39/47] vicinae service: init Signed-off-by: Austin Horstman --- modules/programs/vicinae.nix | 244 ++++++++++++++++++ tests/modules/programs/vicinae/default.nix | 4 + .../programs/vicinae/example-settings.nix | 77 ++++++ 3 files changed, 325 insertions(+) create mode 100644 modules/programs/vicinae.nix create mode 100644 tests/modules/programs/vicinae/default.nix create mode 100644 tests/modules/programs/vicinae/example-settings.nix diff --git a/modules/programs/vicinae.nix b/modules/programs/vicinae.nix new file mode 100644 index 000000000..bde52d4eb --- /dev/null +++ b/modules/programs/vicinae.nix @@ -0,0 +1,244 @@ +{ + config, + pkgs, + lib, + ... +}: +let + cfg = config.programs.vicinae; + + jsonFormat = pkgs.formats.json { }; +in +{ + meta.maintainers = [ lib.maintainers.leiserfg ]; + + options.programs.vicinae = { + enable = lib.mkEnableOption "vicinae launcher daemon"; + + package = lib.mkPackageOption pkgs "vicinae" { nullable = true; }; + + systemd = { + enable = lib.mkEnableOption "vicinae systemd integration"; + + autoStart = lib.mkOption { + type = lib.types.bool; + default = true; + description = "If the vicinae daemon should be started automatically"; + }; + + target = lib.mkOption { + type = lib.types.str; + default = "graphical-session.target"; + example = "sway-session.target"; + description = '' + The systemd target that will automatically start the vicinae service. + ''; + }; + }; + + useLayerShell = lib.mkOption { + type = lib.types.bool; + default = true; + description = "If vicinae should use the layer shell"; + }; + + extensions = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + List of Vicinae extensions to install. + + You can use the `config.lib.vicinae.mkExtension` and `config.lib.vicinae.mkRayCastExtension` functions to create them, like: + ```nix + [ + (config.lib.vicinae.mkExtension { + name = "test-extension"; + src = + pkgs.fetchFromGitHub { + owner = "schromp"; + repo = "vicinae-extensions"; + rev = "f8be5c89393a336f773d679d22faf82d59631991"; + sha256 = "sha256-zk7WIJ19ITzRFnqGSMtX35SgPGq0Z+M+f7hJRbyQugw="; + } + + "/test-extension"; + }) + (config.lib.vicinae.mkRayCastExtension { + name = "gif-search"; + sha256 = "sha256-G7il8T1L+P/2mXWJsb68n4BCbVKcrrtK8GnBNxzt73Q="; + rev = "4d417c2dfd86a5b2bea202d4a7b48d8eb3dbaeb1"; + }) + ], + ``` + ''; + }; + + themes = lib.mkOption { + inherit (jsonFormat) type; + default = { }; + description = '' + Theme settings to add to the themes folder in `~/.config/vicinae/themes`. + + The attribute name of the theme will be the name of theme json file, + e.g. `base16-default-dark` will be `base16-default-dark.json`. + ''; + example = + lib.literalExpression # nix + '' + { + base16-default-dark = { + version = "1.0.0"; + appearance = "dark"; + icon = /path/to/icon.png; + name = "base16 default dark"; + description = "base16 default dark by Chris Kempson"; + palette = { + background = "#181818"; + foreground = "#d8d8d8"; + blue = "#7cafc2"; + green = "#a3be8c"; + magenta = "#ba8baf"; + orange = "#dc9656"; + purple = "#a16946"; + red = "#ab4642"; + yellow = "#f7ca88"; + cyan = "#86c1b9"; + }; + }; + } + ''; + }; + + settings = lib.mkOption { + inherit (jsonFormat) type; + default = { }; + description = "Settings written as JSON to `~/.config/vicinae/vicinae.json."; + example = lib.literalExpression '' + { + faviconService = "twenty"; + font = { + size = 10; + }; + popToRootOnClose = false; + rootSearch = { + searchFiles = false; + }; + theme = { + name = "vicinae-dark"; + }; + window = { + csd = true; + opacity = 0.95; + rounding = 10; + }; + } + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "programs.vicinae" pkgs lib.platforms.linux) + { + assertion = cfg.systemd.enable -> cfg.package != null; + message = "{option}programs.vicinae.systemd.enable requires non null {option}programs.vicinae.package"; + } + ]; + lib.vicinae.mkExtension = ( + { + name, + src, + }: + (pkgs.buildNpmPackage { + inherit name src; + installPhase = '' + runHook preInstall + + mkdir -p $out + cp -r /build/.local/share/vicinae/extensions/${name}/* $out/ + + runHook postInstall + ''; + npmDeps = pkgs.importNpmLock { npmRoot = src; }; + npmConfigHook = pkgs.importNpmLock.npmConfigHook; + }) + ); + + lib.vicinae.mkRayCastExtension = ( + { + name, + sha256, + rev, + }: + let + src = + pkgs.fetchgit { + inherit rev sha256; + url = "https://github.com/raycast/extensions"; + sparseCheckout = [ + "/extensions/${name}" + ]; + } + + "/extensions/${name}"; + in + (pkgs.buildNpmPackage { + inherit name src; + installPhase = '' + runHook preInstall + + mkdir -p $out + cp -r /build/.config/raycast/extensions/${name}/* $out/ + + runHook postInstall + ''; + npmDeps = pkgs.importNpmLock { npmRoot = src; }; + npmConfigHook = pkgs.importNpmLock.npmConfigHook; + }) + ); + + home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; + + xdg = { + configFile = { + "vicinae/vicinae.json" = lib.mkIf (cfg.settings != { }) { + source = jsonFormat.generate "vicinae-settings" cfg.settings; + }; + } + // lib.mapAttrs' ( + name: theme: + lib.nameValuePair "vicinae/themes/${name}.json" { + source = jsonFormat.generate "vicinae-${name}-theme" theme; + } + ) cfg.themes; + + dataFile = builtins.listToAttrs ( + builtins.map (item: { + name = "vicinae/extensions/${item.name}"; + value.source = item; + }) cfg.extensions + ); + }; + + systemd.user.services.vicinae = lib.mkIf (cfg.systemd.enable && cfg.package != null) { + Unit = { + Description = "Vicinae server daemon"; + Documentation = [ "https://docs.vicinae.com" ]; + After = [ cfg.systemd.target ]; + PartOf = [ cfg.systemd.target ]; + BindsTo = [ cfg.systemd.target ]; + }; + Service = { + EnvironmentFile = pkgs.writeText "vicinae-env" '' + USE_LAYER_SHELL=${if cfg.useLayerShell then builtins.toString 1 else builtins.toString 0} + ''; + Type = "simple"; + ExecStart = "${lib.getExe' cfg.package "vicinae"} server"; + Restart = "always"; + RestartSec = 5; + KillMode = "process"; + }; + Install = lib.mkIf cfg.systemd.autoStart { + WantedBy = [ cfg.systemd.target ]; + }; + }; + }; +} diff --git a/tests/modules/programs/vicinae/default.nix b/tests/modules/programs/vicinae/default.nix new file mode 100644 index 000000000..4a4736323 --- /dev/null +++ b/tests/modules/programs/vicinae/default.nix @@ -0,0 +1,4 @@ +{ lib, pkgs, ... }: +lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) { + vicinae-example-settings = ./example-settings.nix; +} diff --git a/tests/modules/programs/vicinae/example-settings.nix b/tests/modules/programs/vicinae/example-settings.nix new file mode 100644 index 000000000..b9de5f24f --- /dev/null +++ b/tests/modules/programs/vicinae/example-settings.nix @@ -0,0 +1,77 @@ +{ + pkgs, + config, + ... +}: + +{ + programs.vicinae = { + enable = true; + systemd.enable = true; + + settings = { + faviconService = "twenty"; + font = { + size = 10; + }; + popToRootOnClose = false; + rootSearch = { + searchFiles = false; + }; + theme = { + name = "vicinae-dark"; + }; + window = { + csd = true; + opacity = 0.95; + rounding = 10; + }; + }; + themes = { + base16-default-dark = { + version = "1.0.0"; + appearance = "dark"; + name = "base16 default dark"; + description = "base16 default dark by Chris Kempson"; + palette = { + background = "#181818"; + foreground = "#d8d8d8"; + blue = "#7cafc2"; + green = "#a3be8c"; + magenta = "#ba8baf"; + orange = "#dc9656"; + purple = "#a16946"; + red = "#ab4642"; + yellow = "#f7ca88"; + cyan = "#86c1b9"; + }; + }; + }; + + extensions = [ + (config.lib.vicinae.mkRayCastExtension { + name = "gif-search"; + sha256 = "sha256-G7il8T1L+P/2mXWJsb68n4BCbVKcrrtK8GnBNxzt73Q="; + rev = "4d417c2dfd86a5b2bea202d4a7b48d8eb3dbaeb1"; + }) + (config.lib.vicinae.mkExtension { + name = "test-extension"; + src = + pkgs.fetchFromGitHub { + owner = "schromp"; + repo = "vicinae-extensions"; + rev = "f8be5c89393a336f773d679d22faf82d59631991"; + sha256 = "sha256-zk7WIJ19ITzRFnqGSMtX35SgPGq0Z+M+f7hJRbyQugw="; + } + + "/test-extension"; + }) + ]; + }; + + nmt.script = '' + assertFileExists "home-files/.config/vicinae/vicinae.json" + assertFileExists "home-files/.config/systemd/user/vicinae.service" + assertFileExists "home-files/.local/share/vicinae/extensions/gif-search/package.json" + assertFileExists "home-files/.local/share/vicinae/extensions/test-extension/package.json" + ''; +} From 6feb3685114e5807b5effe7806b425b75b1b75c0 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 3 Nov 2025 20:46:03 -0600 Subject: [PATCH 40/47] news: add vicinae entry Signed-off-by: Austin Horstman --- .../misc/news/2025/11/2025-11-03_20-33-15.nix | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 modules/misc/news/2025/11/2025-11-03_20-33-15.nix diff --git a/modules/misc/news/2025/11/2025-11-03_20-33-15.nix b/modules/misc/news/2025/11/2025-11-03_20-33-15.nix new file mode 100644 index 000000000..b63b6d6e2 --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_20-33-15.nix @@ -0,0 +1,20 @@ +{ pkgs, ... }: +{ + time = "2025-11-04T02:33:15+00:00"; + condition = pkgs.stdenv.hostPlatform.isLinux; + message = '' + A new program is available: 'programs.vicinae'. + + Vicinae is a modern application launcher daemon for Linux with support for + extensions, custom themes, and layer shell integration. + + The module provides: + - Systemd service integration with automatic start support + - Extension management with helpers for Vicinae and Raycast extensions + - Theme configuration support + - Declarative settings via 'programs.vicinae.settings' + - Layer shell integration for Wayland compositors + + See the module options for more details on configuration. + ''; +} From 65bf99c5793ff83436fa65f64c4cdd874cdb4ebc Mon Sep 17 00:00:00 2001 From: meck Date: Tue, 28 Oct 2025 16:35:02 +0100 Subject: [PATCH 41/47] yazi: update wrappers not to use cat in subshell If cat is aliased to bat the non piping operation in the script might include extra text, use builtins instead --- modules/programs/yazi.nix | 4 ++-- tests/modules/programs/yazi/fish-integration-expected.fish | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/programs/yazi.nix b/modules/programs/yazi.nix index c945447cd..6d8dbe0fb 100644 --- a/modules/programs/yazi.nix +++ b/modules/programs/yazi.nix @@ -221,7 +221,7 @@ in function ${cfg.shellWrapperName}() { local tmp="$(mktemp -t "yazi-cwd.XXXXX")" yazi "$@" --cwd-file="$tmp" - if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then + if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then builtin cd -- "$cwd" fi rm -f -- "$tmp" @@ -231,7 +231,7 @@ in fishIntegration = '' set -l tmp (mktemp -t "yazi-cwd.XXXXX") command yazi $argv --cwd-file="$tmp" - if set cwd (cat -- "$tmp"); and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] + if read cwd < "$tmp"; and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] builtin cd -- "$cwd" end rm -f -- "$tmp" diff --git a/tests/modules/programs/yazi/fish-integration-expected.fish b/tests/modules/programs/yazi/fish-integration-expected.fish index d8f11f284..d0019d999 100644 --- a/tests/modules/programs/yazi/fish-integration-expected.fish +++ b/tests/modules/programs/yazi/fish-integration-expected.fish @@ -1,7 +1,7 @@ function yy set -l tmp (mktemp -t "yazi-cwd.XXXXX") command yazi $argv --cwd-file="$tmp" - if set cwd (cat -- "$tmp"); and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] + if read cwd <"$tmp"; and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] builtin cd -- "$cwd" end rm -f -- "$tmp" From aa6936bb637e46a49cf1292486200ba41dd4bcf7 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 3 Nov 2025 21:51:55 -0600 Subject: [PATCH 42/47] tests/yazi: fix bash/zsh integration tests Didn't fail even with incorrect assertion. Multi line string for assertFileContains didn't properly work. Don't want to manage an entire zsh config file in assertFileContent so just multi step asserting the generated file. Signed-off-by: Austin Horstman --- .../yazi/bash-integration-enabled.nix | 20 ++++++----------- .../programs/yazi/zsh-integration-enabled.nix | 22 +++++++------------ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/tests/modules/programs/yazi/bash-integration-enabled.nix b/tests/modules/programs/yazi/bash-integration-enabled.nix index 1bebbccad..41f56d765 100644 --- a/tests/modules/programs/yazi/bash-integration-enabled.nix +++ b/tests/modules/programs/yazi/bash-integration-enabled.nix @@ -1,15 +1,3 @@ -let - shellIntegration = '' - function yy() { - local tmp="$(mktemp -t "yazi-cwd.XXXXX")" - yazi "$@" --cwd-file="$tmp" - if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then - builtin cd -- "$cwd" - fi - rm -f -- "$tmp" - } - ''; -in { programs.bash.enable = true; @@ -19,6 +7,12 @@ in }; nmt.script = '' - assertFileContains home-files/.bashrc '${shellIntegration}' + assertFileExists home-files/.bashrc + assertFileContains home-files/.bashrc 'function yy() {' + assertFileContains home-files/.bashrc 'local tmp="$(mktemp -t "yazi-cwd.XXXXX")"' + assertFileContains home-files/.bashrc 'yazi "$@" --cwd-file="$tmp"' + assertFileContains home-files/.bashrc 'if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then' + assertFileContains home-files/.bashrc 'builtin cd -- "$cwd"' + assertFileContains home-files/.bashrc 'rm -f -- "$tmp"' ''; } diff --git a/tests/modules/programs/yazi/zsh-integration-enabled.nix b/tests/modules/programs/yazi/zsh-integration-enabled.nix index ef8e0ccc8..6087810c5 100644 --- a/tests/modules/programs/yazi/zsh-integration-enabled.nix +++ b/tests/modules/programs/yazi/zsh-integration-enabled.nix @@ -1,24 +1,18 @@ -let - shellIntegration = '' - function yy() { - local tmp="$(mktemp -t "yazi-cwd.XXXXX")" - yazi "$@" --cwd-file="$tmp" - if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then - builtin cd -- "$cwd" - fi - rm -f -- "$tmp" - } - ''; -in { programs.zsh.enable = true; programs.yazi = { enable = true; - enableBashIntegration = true; + enableZshIntegration = true; }; nmt.script = '' - assertFileContains home-files/.zshrc '${shellIntegration}' + assertFileExists home-files/.zshrc + assertFileContains home-files/.zshrc 'function yy() {' + assertFileContains home-files/.zshrc 'local tmp="$(mktemp -t "yazi-cwd.XXXXX")"' + assertFileContains home-files/.zshrc 'yazi "$@" --cwd-file="$tmp"' + assertFileContains home-files/.zshrc 'if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then' + assertFileContains home-files/.zshrc 'builtin cd -- "$cwd"' + assertFileContains home-files/.zshrc 'rm -f -- "$tmp"' ''; } From c39c07bf31dc080851377c04352fa06f197f0c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Schwarz=C3=A4ugl?= Date: Tue, 4 Nov 2025 12:55:24 +0100 Subject: [PATCH 43/47] opkssh: init module --- .../misc/news/2025/11/2025-11-04_13-00-00.nix | 11 ++++ modules/programs/opkssh.nix | 59 +++++++++++++++++++ tests/modules/programs/opkssh/default.nix | 3 + .../programs/opkssh/opkssh-basic-config.nix | 42 +++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 modules/misc/news/2025/11/2025-11-04_13-00-00.nix create mode 100644 modules/programs/opkssh.nix create mode 100644 tests/modules/programs/opkssh/default.nix create mode 100644 tests/modules/programs/opkssh/opkssh-basic-config.nix diff --git a/modules/misc/news/2025/11/2025-11-04_13-00-00.nix b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix new file mode 100644 index 000000000..8ad7e13dd --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix @@ -0,0 +1,11 @@ +{ + time = "2025-11-04T13:00:00+00:00"; + condition = true; + message = '' + A new module is available: 'programs.opkssh'. + + opkssh is a tool which enables ssh to be used with OpenID Connect allowing SSH access to be managed via identities instead of long-lived SSH keys. It does not replace SSH, but instead generates SSH public keys containing PK Tokens and configures sshd to verify them. These PK Tokens contain standard OpenID Connect ID Tokens. + + This protocol builds on the OpenPubkey which adds user public keys to OpenID Connect without breaking compatibility with existing OpenID Provider. + ''; +} diff --git a/modules/programs/opkssh.nix b/modules/programs/opkssh.nix new file mode 100644 index 000000000..a9d43ea73 --- /dev/null +++ b/modules/programs/opkssh.nix @@ -0,0 +1,59 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.opkssh; + + yamlFormat = pkgs.formats.yaml { }; + +in +{ + meta.maintainers = [ lib.maintainers.swarsel ]; + + options.programs.opkssh = { + enable = lib.mkEnableOption "enable the OpenPubkey SSH client"; + + package = lib.mkPackageOption pkgs "opkssh" { nullable = true; }; + + settings = lib.mkOption { + inherit (yamlFormat) type; + default = { }; + example = lib.literalExpression '' + { + default_provider = "kanidm"; + + providers = [ + { + alias = "kanidm"; + issuer = "https://idm.example.com/oauth2/openid/opkssh"; + client_id = "opkssh"; + scopes = "openid email profile"; + redirect_uris = [ + "http://localhost:3000/login-callback" + "http://localhost:10001/login-callback" + "http://localhost:11110/login-callback" + ]; + }; + ]; + } + ''; + description = '' + Configuration written to {file}`$HOME/.opk/config.yml`. + See . + ''; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; + + home.file."${config.home.homeDirectory}/.opk/config.yml" = lib.mkIf (cfg.settings != { }) { + source = yamlFormat.generate "opkssh-config-${config.home.username}.yml" cfg.settings; + }; + + }; +} diff --git a/tests/modules/programs/opkssh/default.nix b/tests/modules/programs/opkssh/default.nix new file mode 100644 index 000000000..9c6be4912 --- /dev/null +++ b/tests/modules/programs/opkssh/default.nix @@ -0,0 +1,3 @@ +{ + opkssh-basic-config = ./opkssh-basic-config.nix; +} diff --git a/tests/modules/programs/opkssh/opkssh-basic-config.nix b/tests/modules/programs/opkssh/opkssh-basic-config.nix new file mode 100644 index 000000000..a15263b47 --- /dev/null +++ b/tests/modules/programs/opkssh/opkssh-basic-config.nix @@ -0,0 +1,42 @@ +_: { + programs.opkssh = { + enable = true; + settings = { + default_provider = "test-provider"; + providers = [ + { + alias = "test-provider"; + issuer = "https://test.domain/oauth2/openid/opkssh"; + client_id = "opkssh"; + scopes = "openid email profile"; + redirect_uris = [ + "http://localhost:3000/login-callback" + "http://localhost:10001/login-callback" + "http://localhost:11110/login-callback" + ]; + } + ]; + }; + }; + + nmt.script = '' + configFile=home-files/.opk/config.yml + + assertFileExists "$configFile" + + configFileNormalized="$(normalizeStorePaths "$configFile")" + + assertFileContent "$configFileNormalized" ${builtins.toFile "expected.service" '' + default_provider: test-provider + providers: + - alias: test-provider + client_id: opkssh + issuer: https://test.domain/oauth2/openid/opkssh + redirect_uris: + - http://localhost:3000/login-callback + - http://localhost:10001/login-callback + - http://localhost:11110/login-callback + scopes: openid email profile + ''} + ''; +} From 0a5a165aca45dd9c9a8a87b123f1790681f6a3cb Mon Sep 17 00:00:00 2001 From: Ryan Horiguchi Date: Fri, 12 Sep 2025 14:39:03 +0200 Subject: [PATCH 44/47] superfile: add pinnded folder and first use option --- modules/programs/superfile.nix | 100 +++++++++++++++--- .../superfile/example-pinned-folders.json | 6 ++ .../programs/superfile/example-settings.nix | 16 +++ 3 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 tests/modules/programs/superfile/example-pinned-folders.json diff --git a/modules/programs/superfile.nix b/modules/programs/superfile.nix index 4e7765c68..3eb21cfb3 100644 --- a/modules/programs/superfile.nix +++ b/modules/programs/superfile.nix @@ -7,7 +7,10 @@ let cfg = config.programs.superfile; + tomlFormat = pkgs.formats.toml { }; + jsonFormat = pkgs.formats.json { }; + inherit (pkgs.stdenv.hostPlatform) isDarwin; inherit (lib) literalExpression @@ -23,6 +26,29 @@ let types hm ; + + pinnedFolderModule = types.submodule { + freeformType = jsonFormat.type; + + options = { + name = mkOption { + type = types.nullOr types.str; + default = null; + example = "Nix Store"; + description = '' + Name that will be shown. + ''; + }; + + location = mkOption { + type = types.path; + example = "/nix/store"; + description = '' + Location of the pinned entry. + ''; + }; + }; + }; in { meta.maintainers = [ hm.maintainers.LucasWagler ]; @@ -106,11 +132,38 @@ in }; ''; }; + + firstUseCheck = mkOption { + type = types.bool; + default = true; + description = '' + Enables the first time use popup. + ''; + }; + + pinnedFolders = mkOption { + type = types.listOf pinnedFolderModule; + default = [ ]; + example = literalExpression '' + [ + { + name = "Nix Store"; + location = "/nix/store"; + } + ]; + ''; + description = '' + Entries that get added to the pinned panel. + ''; + }; }; config = let enableXdgConfig = !isDarwin || config.xdg.enable; + baseConfigPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile"; + baseDataPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile"; + themeSetting = if (!(cfg.settings ? theme) && cfg.themes != { }) then { @@ -118,7 +171,6 @@ in } else { }; - baseConfigPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile"; configFile = mkIf (cfg.settings != { }) { "${baseConfigPath}/config.toml".source = tomlFormat.generate "superfile-config.toml" ( recursiveUpdate themeSetting cfg.settings @@ -139,24 +191,48 @@ in (tomlFormat.generate "superfile-theme-${name}.toml" value); } ) cfg.themes; + + firstUseCheckFile = mkIf (!cfg.firstUseCheck) { "${baseDataPath}/firstUseCheck".text = ""; }; + pinnedFile = mkIf (cfg.pinnedFolders != [ ]) { + "${baseDataPath}/pinned.json".source = jsonFormat.generate "pinned.json" cfg.pinnedFolders; + }; + + files = mkMerge [ + configFile + hotkeysFile + themeFiles + + firstUseCheckFile + pinnedFile + ]; configFiles = mkMerge [ configFile hotkeysFile themeFiles ]; + dataFiles = mkMerge [ + firstUseCheckFile + pinnedFile + ]; in mkIf cfg.enable { - home.packages = mkIf (cfg.package != null) ( - [ cfg.package ] - ++ optional ( - cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata - ) cfg.metadataPackage - ++ optional ( - cfg.zoxidePackage != null && cfg.settings ? zoxide_support && cfg.settings.zoxide_support - ) cfg.zoxidePackage - ); + home = { + packages = mkIf (cfg.package != null) ( + [ cfg.package ] + ++ optional ( + cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata + ) cfg.metadataPackage + ++ optional ( + cfg.zoxidePackage != null && cfg.settings ? zoxide_support && cfg.settings.zoxide_support + ) cfg.zoxidePackage + ); - xdg.configFile = mkIf enableXdgConfig configFiles; - home.file = mkIf (!enableXdgConfig) configFiles; + file = mkIf (!enableXdgConfig) files; + }; + + xdg = { + configFile = mkIf enableXdgConfig configFiles; + dataFile = mkIf enableXdgConfig dataFiles; + }; }; } diff --git a/tests/modules/programs/superfile/example-pinned-folders.json b/tests/modules/programs/superfile/example-pinned-folders.json new file mode 100644 index 000000000..e7ec94643 --- /dev/null +++ b/tests/modules/programs/superfile/example-pinned-folders.json @@ -0,0 +1,6 @@ +[ + { + "location": "/nix/store", + "name": "Nix Store" + } +] diff --git a/tests/modules/programs/superfile/example-settings.nix b/tests/modules/programs/superfile/example-settings.nix index 8f682b4d2..5b2c96831 100644 --- a/tests/modules/programs/superfile/example-settings.nix +++ b/tests/modules/programs/superfile/example-settings.nix @@ -53,6 +53,13 @@ ]; }; }; + firstUseCheck = false; + pinnedFolders = [ + { + name = "Nix Store"; + location = "/nix/store"; + } + ]; }; nmt.script = @@ -60,6 +67,10 @@ configSubPath = if !pkgs.stdenv.isDarwin then ".config/superfile" else "Library/Application Support/superfile"; configBasePath = "home-files/" + configSubPath; + + dataSubPath = + if !pkgs.stdenv.isDarwin then ".local/share/superfile" else "Library/Application Support/superfile"; + dataBasePath = "home-files/" + dataSubPath; in '' assertFileExists "${configBasePath}/config.toml" @@ -82,5 +93,10 @@ assertFileContent \ "${configBasePath}/theme/test2.toml" \ ${./example-theme2-expected.toml} + assertFileExists "${dataBasePath}/firstUseCheck" + assertFileExists "${dataBasePath}/pinned.json" + assertFileContent \ + "${dataBasePath}/pinned.json" \ + ${./example-pinned-folders.json} ''; } From 1c75dd70229171f47ff10f4ed184101af7c7a392 Mon Sep 17 00:00:00 2001 From: heyzec <61238538+heyzec@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:52:10 +0800 Subject: [PATCH 45/47] vscode: don't break when profile name has spaces --- modules/programs/vscode/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/programs/vscode/default.nix b/modules/programs/vscode/default.nix index 5c89da6e5..fb3064beb 100644 --- a/modules/programs/vscode/default.nix +++ b/modules/programs/vscode/default.nix @@ -459,7 +459,7 @@ in existing_profiles=$(jq '.userDataProfiles // [] | map({ (.name): .location }) | add // {}' "$file") for profile in "''${profiles[@]}"; do - if [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" != "true" ]] || [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" == "true" && "$(echo $existing_profiles | jq --arg profile $profile '.[$profile]')" != "\"$profile\"" ]]; then + if [[ "$(echo $existing_profiles | jq --arg profile "$profile" 'has ($profile)')" != "true" ]] || [[ "$(echo $existing_profiles | jq --arg profile "$profile" 'has ($profile)')" == "true" && "$(echo $existing_profiles | jq --arg profile "$profile" '.[$profile]')" != "\"$profile\"" ]]; then file_write="$file_write$([ "$file_write" != "" ] && echo "...")$profile" fi done From 34fe48801d2a5301b814eaa1efb496499d06cebc Mon Sep 17 00:00:00 2001 From: Martijn Boers Date: Mon, 3 Nov 2025 21:33:55 +0100 Subject: [PATCH 46/47] ghostty: Add systemd integration The systemd unit already exists on the system, this gives the option to enable it. --- .../misc/news/2025/11/2025-11-03_22-56-50.nix | 14 ++++++++ modules/programs/ghostty.nix | 34 +++++++++++++++++++ .../programs/ghostty/empty-settings.nix | 6 +++- .../programs/ghostty/example-settings.nix | 2 +- .../programs/ghostty/example-theme.nix | 2 ++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 modules/misc/news/2025/11/2025-11-03_22-56-50.nix diff --git a/modules/misc/news/2025/11/2025-11-03_22-56-50.nix b/modules/misc/news/2025/11/2025-11-03_22-56-50.nix new file mode 100644 index 000000000..d78de2292 --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_22-56-50.nix @@ -0,0 +1,14 @@ +{ config, pkgs, ... }: +{ + time = "2025-11-03T21:56:50+00:00"; + condition = config.programs.ghostty.enable && pkgs.stdenv.hostPlatform.isLinux; + message = '' + Ghostty: now enables the user systemd service by default. + + Running Ghostty via these systemd units is the recommended way to run + Ghostty. The two most important benefits provided by Ghostty's systemd + integrations are: instantaneous launching and centralized logging. + + See https://ghostty.org/docs/linux/systemd for all details + ''; +} diff --git a/modules/programs/ghostty.nix b/modules/programs/ghostty.nix index 43f3d99fe..38cfadf1a 100644 --- a/modules/programs/ghostty.nix +++ b/modules/programs/ghostty.nix @@ -114,6 +114,24 @@ in defaultText = lib.literalMD "`true` if programs.ghostty.package is not null"; }; + systemd = lib.mkOption { + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "the Ghostty systemd user service" // { + default = pkgs.stdenv.hostPlatform.isLinux; + defaultText = lib.literalMD "`true` on Linux, `false` otherwise"; + }; + }; + }; + default = { }; + description = '' + Configuration for Ghostty's systemd integration. + This enables additional speed and features. + + See for more information. + ''; + }; + enableBashIntegration = mkShellIntegrationOption ( lib.hm.shell.mkBashIntegrationOption { inherit config; } ); @@ -195,6 +213,22 @@ in }; }) + (lib.mkIf cfg.systemd.enable { + assertions = [ + { + assertion = cfg.systemd.enable -> cfg.package != null; + message = "programs.ghostty.systemd.enable cannot be true when programs.ghostty.package is null"; + } + { + assertion = cfg.systemd.enable -> pkgs.stdenv.hostPlatform.isLinux; + message = "Ghostty systemd integration cannot be enabled for non-linux platforms"; + } + ]; + xdg.configFile."systemd/user/app-com.mitchellh.ghostty.service".source = + "${cfg.package}/share/systemd/user/app-com.mitchellh.ghostty.service"; + dbus.packages = [ cfg.package ]; + }) + (lib.mkIf cfg.enableBashIntegration { # Make order 101 to be placed exactly after bash completions, as Ghostty # documentation suggests sourcing the script as soon as possible diff --git a/tests/modules/programs/ghostty/empty-settings.nix b/tests/modules/programs/ghostty/empty-settings.nix index 1ea025eb5..d74972a4a 100644 --- a/tests/modules/programs/ghostty/empty-settings.nix +++ b/tests/modules/programs/ghostty/empty-settings.nix @@ -1,5 +1,9 @@ +{ config, ... }: { - programs.ghostty.enable = true; + programs.ghostty = { + enable = true; + package = config.lib.test.mkStubPackage { outPath = null; }; + }; nmt.script = '' assertPathNotExists home-files/.config/ghostty/config diff --git a/tests/modules/programs/ghostty/example-settings.nix b/tests/modules/programs/ghostty/example-settings.nix index 18b64b4fe..b45094d2f 100644 --- a/tests/modules/programs/ghostty/example-settings.nix +++ b/tests/modules/programs/ghostty/example-settings.nix @@ -2,7 +2,7 @@ { programs.ghostty = { enable = true; - package = config.lib.test.mkStubPackage { }; + package = config.lib.test.mkStubPackage { outPath = null; }; settings = { theme = "catppuccin-mocha"; diff --git a/tests/modules/programs/ghostty/example-theme.nix b/tests/modules/programs/ghostty/example-theme.nix index 29f40a897..320b073a1 100644 --- a/tests/modules/programs/ghostty/example-theme.nix +++ b/tests/modules/programs/ghostty/example-theme.nix @@ -1,6 +1,8 @@ +{ config, ... }: { programs.ghostty = { enable = true; + package = config.lib.test.mkStubPackage { outPath = null; }; themes = { catppuccin-mocha = { From d6ea5063aecddba4935c5dd2d70d8cf381131ef1 Mon Sep 17 00:00:00 2001 From: "home-manager-ci[bot]" <214323736+home-manager-ci[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 04:11:21 +0000 Subject: [PATCH 47/47] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15?narHash=sha256-kJ8lIZsiPOmbkJypG%2BB5sReDXSD1KGu2VEPNqhRa/ew%3D' (2025-10-31) → 'github:NixOS/nixpkgs/b3d51a0365f6695e7dd5cdf3e180604530ed33b4?narHash=sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw%3D' (2025-11-02) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 0a8f512ba..35822786c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1761907660, - "narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", + "lastModified": 1762111121, + "narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", + "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", "type": "github" }, "original": {