Commit 26744f7

HPCesia <me@hpcesia.com>
2026-08-03 13:32:38
den: add per-aspect settings to host and user
Assisted-by: opencode:deepseek-v4-flash
1 parent 93b1a5d
Changed files (2)
modules/users/schema.nix
@@ -27,6 +27,16 @@
             };
             default = {};
           };
+          system = lib.mkOption {
+            type = lib.types.submodule {
+              options = {
+                settings = lib.mkOption {
+                  type = lib.types.anything;
+                };
+              };
+            };
+            default = {};
+          };
         };
       })
     );
modules/mk-settings.nix
@@ -0,0 +1,129 @@
+# Per-aspect settings surface for hosts and users.
+#
+#   den.aspects.<name>.settings.host = { <opt> = lib.mkOption ...; };
+#   den.aspects.<name>.settings.user = { <opt> = lib.mkOption ...; };
+#
+# Collected into the schemas as typed options:
+#
+#   den.schema.host.options.settings.<aspect-path>.<option>
+#   den.schema.user.options.system.settings.<aspect-path>.<option>
+#
+# Hosts/users provide values at the same path on their entities, and
+# aspects read them from their context. User values merge the shared
+# `den.users.<username>` registry first, as lowest-priority definitions.
+{
+  config,
+  lib,
+  ...
+}: let
+  # Pipeline-special keys; skip when walking for settings declarations.
+  structuralKeys = [
+    "excludes"
+    "includes"
+    "meta"
+    "name"
+    "provides"
+  ];
+
+  # Collect { prefix, host, user } for aspect nodes declaring settings.
+  # Bare-function aspects cannot carry a settings attr and are skipped.
+  collect = prefix: node:
+    if !(builtins.isAttrs node)
+    then []
+    else let
+      settings = let
+        s = node.settings or null;
+      in
+        if builtins.isAttrs s
+        then s
+        else null;
+      hasHost = builtins.isAttrs (settings.host or null);
+      hasUser = builtins.isAttrs (settings.user or null);
+      mine = lib.optional (hasHost || hasUser) {
+        inherit prefix;
+        host = lib.optionalAttrs hasHost settings.host;
+        user = lib.optionalAttrs hasUser settings.user;
+      };
+      children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
+        builtins.filter (name: !(builtins.elem name structuralKeys) && name != "settings") (
+          builtins.attrNames node
+        )
+      );
+    in
+      mine ++ children;
+
+  collected = collect [] config.den.aspects;
+
+  # Build nested option declarations, grouping shared path prefixes.
+  mkOptions = nodes: let
+    grouped = lib.groupBy (n: builtins.head n.path) nodes;
+  in
+    lib.mapAttrs (
+      name: group:
+        if lib.all (n: builtins.length n.path == 1) group
+        then
+          lib.mkOption {
+            type = lib.types.submodule {
+              options = (builtins.head group).decls;
+            };
+            default = {};
+            description = "Settings for aspect `${name}'.";
+          }
+        else if !(lib.all (n: builtins.length n.path > 1) group)
+        then throw "aspect settings: aspect `${name}' is both a settings leaf and a container"
+        else mkOptions (map (n: n // {path = lib.drop 1 n.path;}) group)
+    )
+    grouped;
+
+  hostNodes = map (n: {
+    path = n.prefix;
+    decls = n.host;
+  }) (lib.filter (n: n.host != {}) collected);
+
+  userNodes = map (n: {
+    path = n.prefix;
+    decls = n.user;
+  }) (lib.filter (n: n.user != {}) collected);
+
+  userSettingsType = lib.types.submodule {
+    # The shared registry and hosts also carry raw user-system values
+    freeformType = lib.types.attrsOf lib.types.anything;
+    options.settings = mkOptions userNodes;
+  };
+
+  # Merge the shared `den.users.<username>` values as lowest-priority defs.
+  registryAware = inner:
+    lib.mkOptionType {
+      name = "den-shared-user-system-settings";
+      check = v: builtins.isAttrs v;
+      merge = loc: defs: let
+        username = lib.elemAt loc (builtins.length loc - 2);
+        shared = config.den.users.${username}.system or {};
+      in
+        inner.merge loc ([
+            {
+              file = "<den.users>";
+              value = shared;
+            }
+          ]
+          ++ defs);
+    };
+in {
+  config = {
+    den.reservedKeys = ["settings"];
+
+    den.schema.host.options.settings = lib.mkOption {
+      type = lib.types.submodule {
+        options = mkOptions hostNodes;
+      };
+      default = {};
+      description = "Host-provided settings for den aspects.";
+    };
+
+    den.schema.user.options.system = lib.mkOption {
+      type = registryAware userSettingsType;
+      default = {};
+      description = "User-provided settings for den aspects.";
+    };
+  };
+}