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}