main
  1{
  2  den,
  3  lib,
  4  ...
  5}: let
  6  # Walk den.aspects (at pipeline time) for .themes sub-aspects
  7  # and include the ones selected in user.system.themes.
  8  # user.system.themes.theme = global default; per-aspect keys override it.
  9  themeIncludes = {user, ...}: let
 10    themesCfg = user.system.themes or {};
 11    globalTheme = themesCfg.theme or null;
 12
 13    inherit (den.lib.aspects.fx.keyClassification) structuralKeysSet;
 14    classKeys = den.classes or {};
 15    quirkKeys = den.quirks or {};
 16    skipKey = k: lib.hasPrefix "__" k || structuralKeysSet ? ${k} || classKeys ? ${k} || quirkKeys ? ${k} || k == "settings" || k == "provides";
 17
 18    # Collect paths of aspects that have .themes (non-recursive flat walk).
 19    collectThemePaths = let
 20      go = prefix: node:
 21        if !builtins.isAttrs node
 22        then []
 23        else let
 24          hasThemes = node ? themes && builtins.isAttrs node.themes;
 25          pathStr = lib.concatStringsSep "." prefix;
 26          here =
 27            if hasThemes
 28            then [pathStr]
 29            else [];
 30          children = lib.concatMap (
 31            k:
 32              if skipKey k
 33              then []
 34              else go (prefix ++ [k]) node.${k}
 35          ) (builtins.attrNames node);
 36        in
 37          here ++ children;
 38    in
 39      go [] (den.aspects or {});
 40
 41    # Resolve effective theme for an aspect path — walks the tree structure.
 42    effectiveTheme = path: let
 43      segs = lib.splitString "." path;
 44      explicit = lib.attrByPath segs null themesCfg;
 45    in
 46      if explicit != null
 47      then explicit
 48      else globalTheme;
 49
 50    includeThemeAspect = path: theme: let
 51      segs = lib.splitString "." path;
 52      parentAspect = lib.attrByPath segs null den.aspects;
 53      themeAspect = lib.attrByPath (segs ++ ["themes" theme]) null den.aspects;
 54      # some den.aspects.<path> node carries no name/meta.provider
 55      parentRef = {
 56        name = lib.last segs;
 57        meta.provider = lib.init segs;
 58      };
 59    in
 60      lib.optionals (themeAspect != null && parentAspect != null) (
 61        den.lib.policy.when
 62        ({user, ...}: user.hasAspect parentRef)
 63        [themeAspect]
 64      );
 65  in {
 66    name = "users/themes";
 67    includes =
 68      lib.concatMap (
 69        path: let
 70          t = effectiveTheme path;
 71        in
 72          if t != null
 73          then includeThemeAspect path t
 74          else []
 75      )
 76      collectThemePaths;
 77  };
 78in {
 79  den.default.includes = [
 80    den.batteries.define-user
 81    ({user, ...}: {
 82      name = "users/description";
 83      user.description = user.identity.displayName;
 84    })
 85    ({user, ...}: {
 86      name = "users/avatar";
 87      nixos = lib.optionalAttrs (user.identity.avatar != null) {
 88        systemd.tmpfiles.rules = [
 89          "d /var/lib/AccountsService/icons 0755 root root -"
 90          "C /var/lib/AccountsService/icons/${user.userName}.png - - - - ${user.identity.avatar}"
 91        ];
 92      };
 93    })
 94    ({user, ...}: {
 95      name = "users/password";
 96      nixos = {config, ...}: let
 97        secretName = "hashed-password-user-${user.userName}";
 98      in {
 99        users.users.${user.userName}.hashedPasswordFile = config.vaultix.secrets.${secretName}.path;
100        vaultix.secrets.${secretName}.file = user.system.hashedPasswordAged;
101        vaultix.beforeUserborn = [secretName];
102      };
103    })
104    themeIncludes
105  ];
106}