main
  1# Settings surface shared by hosts and users: aspects declare
  2# `settings.{host,user}`, entities supply values, aspects read them back.
  3{
  4  config,
  5  den,
  6  lib,
  7  ...
  8}: let
  9  inherit (den.lib.aspects.fx.keyClassification) isStructuralKey;
 10
 11  skipKey = name:
 12    isStructuralKey name
 13    || (den.classes or {}) ? ${name}
 14    || (den.quirks or {}) ? ${name};
 15
 16  # Root names are aspect names, so a reserved one such as `themes` stays
 17  # walkable there, and only keys inside an aspect are classified.
 18  isChildKey = prefix: name: name != "settings" && (prefix == [] || !(skipKey name));
 19
 20  reshape = raw:
 21    if raw ? options || raw ? config || raw ? imports
 22    then raw
 23    else {options = raw;};
 24
 25  # Bare-function aspects cannot carry a settings attr and are skipped.
 26  collect = prefix: node:
 27    if !(builtins.isAttrs node)
 28    then []
 29    else let
 30      settings = let
 31        s = node.settings or null;
 32      in
 33        if builtins.isAttrs s
 34        then s
 35        else null;
 36      isDecl = v: builtins.isAttrs v || builtins.isFunction v;
 37      hasHost = isDecl (settings.host or null);
 38      hasUser = isDecl (settings.user or null);
 39      mine = lib.optional (hasHost || hasUser) {
 40        inherit prefix;
 41        host =
 42          if hasHost
 43          then settings.host
 44          else {};
 45        user =
 46          if hasUser
 47          then settings.user
 48          else {};
 49      };
 50      children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
 51        builtins.filter (isChildKey prefix) (builtins.attrNames node)
 52      );
 53    in
 54      mine ++ children;
 55
 56  collected = collect [] config.den.aspects;
 57
 58  # `ctx` is the entity instance, and forcing it here would recurse (gen-schema ADR-0033),
 59  # so only lazy reads such as defaults may touch it.
 60  mkOptions = ctx: nodes: let
 61    grouped = lib.groupBy (n: builtins.head n.path) nodes;
 62  in
 63    lib.mapAttrs (
 64      name: group: let
 65        own = lib.filter (n: builtins.length n.path == 1) group;
 66        children = lib.filter (n: builtins.length n.path > 1) group;
 67        ownModule =
 68          if own == []
 69          then {}
 70          else let
 71            raw = (builtins.head own).decls;
 72          in
 73            reshape (
 74              if builtins.isFunction raw
 75              then raw ctx
 76              else raw
 77            );
 78        childOptions =
 79          if children == []
 80          then {}
 81          else mkOptions ctx (map (n: n // {path = lib.drop 1 n.path;}) children);
 82      in
 83        lib.mkOption {
 84          type = lib.types.submodule (ownModule // {options = (ownModule.options or {}) // childOptions;});
 85          default = {};
 86          description = "Settings for aspect `${name}'.";
 87        }
 88    )
 89    grouped;
 90
 91  hostNodes = map (n: {
 92    path = n.prefix;
 93    decls = n.host;
 94  }) (lib.filter (n: n.host != {}) collected);
 95
 96  userNodes = map (n: {
 97    path = n.prefix;
 98    decls = n.user;
 99  }) (lib.filter (n: n.user != {}) collected);
100
101  userSettingsType = ctx:
102    lib.types.submodule {
103      # The shared registry may carry keys outside `settings`.
104      freeformType = lib.types.attrsOf lib.types.anything;
105      options.settings = mkOptions ctx userNodes;
106    };
107
108  # `den.users.<username>` values merge as the lowest-priority definition.
109  registryAware = inner:
110    lib.mkOptionType {
111      name = "den-shared-user-system-settings";
112      check = v: builtins.isAttrs v;
113      merge = loc: defs: let
114        username = lib.elemAt loc (builtins.length loc - 2);
115        shared = config.den.users.${username}.system or {};
116      in
117        inner.merge loc ([
118            {
119              file = "<den.users>";
120              value = shared;
121            }
122          ]
123          ++ defs);
124    };
125in {
126  config = {
127    den.reservedKeys = ["settings"];
128
129    # Built per instance through imports so function declarations receive entity.
130    den.schema.host.imports = [
131      ({host, ...}: {
132        options.settings = lib.mkOption {
133          type = lib.types.submodule {
134            options = mkOptions {inherit host;} hostNodes;
135          };
136          default = {};
137          description = "Host-provided settings for den aspects.";
138        };
139      })
140    ];
141
142    den.schema.user.imports = [
143      ({
144        host,
145        user,
146        ...
147      }: {
148        options.system = lib.mkOption {
149          type = registryAware (userSettingsType {inherit host user;});
150          default = {};
151          description = "User-provided settings for den aspects.";
152        };
153      })
154    ];
155  };
156}