main
 1# Auto collect all registered theme config from `den.aspects.<path>.themes.<name>`
 2# by `den.users.<name>.system.settings.themes.theme`.
 3{
 4  den,
 5  lib,
 6  ...
 7}: let
 8  scanSkip = [
 9    "_"
10    "_module"
11    "classes"
12    "excludes"
13    "includes"
14    "meta"
15    "name"
16    "policies"
17    "provides"
18    "settings"
19    "themes"
20  ];
21
22  # Navigated refs lack name/meta; derive them from __aspectChain so
23  # guard-time hasAspect matches what the walker assigns.
24  normalizeRef = v:
25    if
26      builtins.isAttrs v
27      && !(v ? name)
28      && builtins.isList (v.__aspectChain or [])
29      && v.__aspectChain != []
30    then
31      v
32      // {
33        name = lib.last v.__aspectChain;
34        meta =
35          (v.meta or {})
36          // {
37            aspect-chain = lib.init v.__aspectChain;
38          };
39      }
40    else v;
41
42  # Collect every `themes.<name>` aspect in the tree.
43  collectThemes = path: node:
44    if !(builtins.isAttrs node)
45    then []
46    else let
47      registry = node.themes or null;
48      mine =
49        if builtins.isAttrs registry
50        then
51          lib.mapAttrsToList (name: theme: {
52            inherit name;
53            owner = node;
54            inherit theme;
55          })
56          registry
57        else [];
58      recurse = builtins.concatLists (map (k: collectThemes (path ++ [k]) node.${k}) (
59        builtins.filter (
60          k:
61            !(builtins.elem k scanSkip)
62            && builtins.isAttrs node.${k}
63            && !(lib.hasPrefix "__" k)
64        ) (builtins.attrNames node)
65      ));
66    in
67      mine ++ recurse;
68
69  candidates = collectThemes [] den.aspects;
70
71  # Re-emit the user's matching theme registrations as aspects guarded
72  # on their owner.
73  controller = {
74    host,
75    user,
76    ...
77  }: let
78    theme = user.system.settings.themes.theme or null;
79    matched = builtins.filter (t: t.name == theme) candidates;
80  in {
81    name = "themes-controller/${user.userName}@${host.name}";
82    includes = map (t:
83      den.lib.policy.when ({host, ...}: host.hasAspect (normalizeRef t.owner)) t.theme)
84    matched;
85  };
86in {
87  den.reservedKeys = ["themes"];
88
89  den.aspects.themes.settings.user.theme = lib.mkOption {
90    type = lib.types.nullOr lib.types.str;
91    default = null;
92    description = "Color theme to activate for this user. Matching `themes.<name>` aspects are imported automatically.";
93  };
94
95  den.default.includes = [controller];
96}