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}