From 6a8444467c83c961e2f5ff64fb4f422e303c98d3 Mon Sep 17 00:00:00 2001
From: Olli Helenius <liff@iki.fi>
Date: Tue, 7 Nov 2023 16:55:17 +0200
Subject: [PATCH] systemd: add settings option (#4276)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The `systemd.user.extraConfig` provides a way to generate a
`systemd-user.conf(5)` file for the user.

This is the home-manager equivalent of NixOS’s option of the same
name, with the difference that NixOS’s option generates a `user.conf`
file that is shared between all users.
---
 modules/systemd.nix                   | 71 +++++++++++++++++++++++++++
 tests/modules/systemd/default.nix     |  1 +
 tests/modules/systemd/user-config.nix | 25 ++++++++++
 3 files changed, 97 insertions(+)
 create mode 100644 tests/modules/systemd/user-config.nix

diff --git a/modules/systemd.nix b/modules/systemd.nix
index 22490218..18e88819 100644
--- a/modules/systemd.nix
+++ b/modules/systemd.nix
@@ -6,10 +6,16 @@ let
 
   inherit (lib) getAttr hm isBool literalExpression mkIf mkMerge mkOption types;
 
+  settingsFormat = pkgs.formats.ini { listsAsDuplicateKeys = true; };
+
   # From <nixpkgs/nixos/modules/system/boot/systemd-lib.nix>
   mkPathSafeName =
     lib.replaceStrings [ "@" ":" "\\" "[" "]" ] [ "-" "-" "-" "" "" ];
 
+  removeIfEmpty = attrs: names:
+    lib.filterAttrs (name: value: !(builtins.elem name names) || value != "")
+    attrs;
+
   toSystemdIni = lib.generators.toINI {
     listsAsDuplicateKeys = true;
     mkKeyValue = key: value:
@@ -87,6 +93,11 @@ let
       + "\n";
   };
 
+  settings = mkIf (cfg.settings != { }) {
+    "systemd/user.conf".source =
+      settingsFormat.generate "user.conf" cfg.settings;
+  };
+
 in {
   meta.maintainers = [ lib.maintainers.rycee ];
 
@@ -209,6 +220,64 @@ in {
           {manpage}`environment.d(5)`.
         '';
       };
+
+      settings = mkOption {
+        apply = sections:
+          sections // {
+            # Setting one of these to an empty value would reset any
+            # previous settings, so we’ll remove them instead if they
+            # are not explicitly set.
+            Manager = removeIfEmpty sections.Manager [
+              "ManagerEnvironment"
+              "DefaultEnvironment"
+            ];
+          };
+
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+
+          options = let
+            inherit (lib) concatStringsSep escapeShellArg mapAttrsToList;
+            environmentOption = args:
+              mkOption {
+                type = with types;
+                  attrsOf (nullOr (oneOf [ str path package ]));
+                default = { };
+                example = literalExpression ''
+                  {
+                    PATH = "%u/bin:%u/.cargo/bin";
+                  }
+                '';
+                apply = value:
+                  concatStringsSep " "
+                  (mapAttrsToList (n: v: "${n}=${escapeShellArg v}") value);
+              } // args;
+          in {
+            Manager = {
+              DefaultEnvironment = environmentOption {
+                description = ''
+                  Configures environment variables passed to all executed processes.
+                '';
+              };
+              ManagerEnvironment = environmentOption {
+                description = ''
+                  Sets environment variables just for the manager process itself.
+                '';
+              };
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            Manager.DefaultCPUAccounting = true;
+          }
+        '';
+        description = ''
+          Extra config options for user session service manager. See {manpage}`systemd-user.conf(5)` for
+          available options.
+        '';
+      };
     };
   };
 
@@ -227,6 +296,8 @@ in {
         ++ (buildServices "automount" cfg.automounts)))
 
       sessionVariables
+
+      settings
     ];
 
     # Run systemd service reload if user is logged in. If we're
diff --git a/tests/modules/systemd/default.nix b/tests/modules/systemd/default.nix
index a0271b47..250b8c79 100644
--- a/tests/modules/systemd/default.nix
+++ b/tests/modules/systemd/default.nix
@@ -2,6 +2,7 @@
   systemd-services = ./services.nix;
   systemd-services-disabled-for-root = ./services-disabled-for-root.nix;
   systemd-session-variables = ./session-variables.nix;
+  systemd-user-config = ./user-config.nix;
   systemd-slices = ./slices.nix;
   systemd-timers = ./timers.nix;
 }
diff --git a/tests/modules/systemd/user-config.nix b/tests/modules/systemd/user-config.nix
new file mode 100644
index 00000000..f977d2f1
--- /dev/null
+++ b/tests/modules/systemd/user-config.nix
@@ -0,0 +1,25 @@
+{ pkgs, ... }:
+
+{
+  systemd.user.settings.Manager = {
+    LogLevel = "debug";
+    DefaultCPUAccounting = true;
+    DefaultEnvironment = {
+      TEST = "abc";
+      PATH = "/bin:/sbin:/some where";
+    };
+  };
+
+  nmt.script = ''
+    userConf=home-files/.config/systemd/user.conf
+    assertFileExists $userConf
+    assertFileContent $userConf ${
+      pkgs.writeText "expected" ''
+        [Manager]
+        DefaultCPUAccounting=true
+        DefaultEnvironment=PATH='/bin:/sbin:/some where' TEST='abc'
+        LogLevel=debug
+      ''
+    }
+  '';
+}