main
1# Per-aspect settings surface for hosts and users.
2#
3# den.aspects.<name>.settings.host = { <opt> = lib.mkOption ...; };
4# den.aspects.<name>.settings.user = { <opt> = lib.mkOption ...; };
5#
6# Collected into the schemas as typed options:
7#
8# den.schema.host.options.settings.<aspect-path>.<option>
9# den.schema.user.options.system.settings.<aspect-path>.<option>
10#
11# Hosts/users provide values at the same path on their entities, and
12# aspects read them from their context. User values merge the shared
13# `den.users.<username>` registry first, as lowest-priority definitions.
14{
15 config,
16 lib,
17 ...
18}: let
19 # Pipeline-special keys; skip when walking for settings declarations.
20 structuralKeys = [
21 "excludes"
22 "includes"
23 "meta"
24 "name"
25 "provides"
26 "themes" # See modules/users/theme.nix
27 ];
28
29 # Collect { prefix, host, user } for aspect nodes declaring settings.
30 # Bare-function aspects cannot carry a settings attr and are skipped.
31 collect = prefix: node:
32 if !(builtins.isAttrs node)
33 then []
34 else let
35 settings = let
36 s = node.settings or null;
37 in
38 if builtins.isAttrs s
39 then s
40 else null;
41 hasHost = builtins.isAttrs (settings.host or null);
42 hasUser = builtins.isAttrs (settings.user or null);
43 mine = lib.optional (hasHost || hasUser) {
44 inherit prefix;
45 host = lib.optionalAttrs hasHost settings.host;
46 user = lib.optionalAttrs hasUser settings.user;
47 };
48 children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
49 builtins.filter (name:
50 !(builtins.elem name (
51 if prefix == []
52 then []
53 else structuralKeys
54 ))
55 && name != "settings") (
56 builtins.attrNames node
57 )
58 );
59 in
60 mine ++ children;
61
62 collected = collect [] config.den.aspects;
63
64 # Build nested option declarations, grouping shared path prefixes.
65 mkOptions = nodes: let
66 grouped = lib.groupBy (n: builtins.head n.path) nodes;
67 in
68 lib.mapAttrs (
69 name: group:
70 if lib.all (n: builtins.length n.path == 1) group
71 then
72 lib.mkOption {
73 type = lib.types.submodule {
74 options = (builtins.head group).decls;
75 };
76 default = {};
77 description = "Settings for aspect `${name}'.";
78 }
79 else if !(lib.all (n: builtins.length n.path > 1) group)
80 then throw "aspect settings: aspect `${name}' is both a settings leaf and a container"
81 else mkOptions (map (n: n // {path = lib.drop 1 n.path;}) group)
82 )
83 grouped;
84
85 hostNodes = map (n: {
86 path = n.prefix;
87 decls = n.host;
88 }) (lib.filter (n: n.host != {}) collected);
89
90 userNodes = map (n: {
91 path = n.prefix;
92 decls = n.user;
93 }) (lib.filter (n: n.user != {}) collected);
94
95 userSettingsType = lib.types.submodule {
96 # The shared registry and hosts also carry raw user-system values
97 freeformType = lib.types.attrsOf lib.types.anything;
98 options.settings = mkOptions userNodes;
99 };
100
101 # Merge the shared `den.users.<username>` values as lowest-priority defs.
102 registryAware = inner:
103 lib.mkOptionType {
104 name = "den-shared-user-system-settings";
105 check = v: builtins.isAttrs v;
106 merge = loc: defs: let
107 username = lib.elemAt loc (builtins.length loc - 2);
108 shared = config.den.users.${username}.system or {};
109 in
110 inner.merge loc ([
111 {
112 file = "<den.users>";
113 value = shared;
114 }
115 ]
116 ++ defs);
117 };
118in {
119 config = {
120 den.reservedKeys = ["settings"];
121
122 den.schema.host.options.settings = lib.mkOption {
123 type = lib.types.submodule {
124 options = mkOptions hostNodes;
125 };
126 default = {};
127 description = "Host-provided settings for den aspects.";
128 };
129
130 den.schema.user.options.system = lib.mkOption {
131 type = registryAware userSettingsType;
132 default = {};
133 description = "User-provided settings for den aspects.";
134 };
135 };
136}