From 3f82256e45bae2d4002120735033b2a5ec5694bc Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Thu, 19 Mar 2026 13:46:47 -0600 Subject: [PATCH] feat(class): Allow top-level forwarded-alias classes --- .../content/docs/guides/custom-classes.mdx | 89 ++++++++-- modules/aspects/provides/forward.nix | 38 ++++- .../modules/features/forward-alias-class.nix | 154 ++++++++++++++++++ 3 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 templates/ci/modules/features/forward-alias-class.nix diff --git a/docs/src/content/docs/guides/custom-classes.mdx b/docs/src/content/docs/guides/custom-classes.mdx index b0ede102d..97d30190f 100644 --- a/docs/src/content/docs/guides/custom-classes.mdx +++ b/docs/src/content/docs/guides/custom-classes.mdx @@ -6,9 +6,11 @@ description: Create new classes via den.provides.forward. import { Aside } from '@astrojs/starlight/components'; ## What is a Custom Class @@ -44,13 +46,14 @@ den.provides.forward { | `each` | List of items to forward (typically `[ user ]` or `[ true ]`) | | `fromClass` | The custom class name to read from | | `intoClass` | The target class to write into | -| `intoPath` | Target attribute path in the target class | +| `intoPath` | Target attribute path in the target class| | `fromAspect` | The aspect to read the custom class from | + ## Example: The Built-in `user` Class -The `user` class (`modules/aspects/provides/os-user.nix`) forwards OS-level -user settings without requiring Home Manager: +The `user` class ([`provides/os-user.nix`](https://github.com/vic/den/tree/main/modules/aspects/provides/os-user.nix)) +forwards OS-level user settings to NixOS/nix-Darwin lightweight user-environment. ```nix # Instead of: @@ -156,7 +159,71 @@ guard = { config, ... }: _item: lib.mkIf (config.programs.vim.enable); ## User contributed examples -#### Example: Config across `nixos` and `darwin` classes. +### Example: Alias a Class into the Target Root + +This pattern is useful when you want a class to behave like an alias for another +class while keeping a separate name in your aspects. + +```nix +hmAlias = + { class, aspect-chain }: + den._.forward { + each = lib.singleton class; + fromClass = _: "hm"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + adaptArgs = { config, ... }: { osConfig = config; }; + }; + +den.aspects.tux = { + includes = [ hmAlias ]; + hm = + { osConfig, ... }: + { + programs.fish.enable = true; + home.keyboard.model = osConfig.networking.hostName; + }; +}; +``` + +This forwards `hm.*` directly into `homeManager.*`. A more interesting use case is the following: + + +### Example: Platform specific `hm` classes + +This pattern is useful when you need HM to distinguish between +different OS Platforms, because some packages only build in +Darwin and not NixOS. + + +```nix +hmPlatforms = + { class, aspect-chain }: + den._.forward { + each = [ "Linux" "Darwin" "Aarch64" "64bit" ]; + fromClass = platform: "hm${platform}"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + guard = { pkgs, ... }: platform: lib.mkIf pkgs.stdenv."is${platform}"; + adaptArgs = { config, ... }: { osConfig = config; }; + }; + +den.aspects.tux = { + includes = [ hmPlatforms ]; + + hmLinux = { pkgs, ... }: { + home.packages = [ pkgs.wl-clipboard-rs ]; + }; + + hmDarwin = { pkgs, ... }: { + home.packages = [ pkgs.iterm2 ]; + }; +}; +``` + +### Example: Config across `nixos` and `darwin` classes. The `os` forward class ([provided by Den](https://github.com/vic/den/blob/main/modules/aspects/provides/os-class.nix)) can be useful for settings that must be forwarded to both on NixOS and MacOS. @@ -180,7 +247,7 @@ den.aspects.my-laptop = { }; ``` -#### Example: Role based configuration between users and hosts +### Example: Role based configuration between users and hosts A dynamic class for matching roles between users and hosts. @@ -215,7 +282,7 @@ den.aspects.alice = { }; ``` -#### Example: A git class that checks enable. +### Example: A git class that checks enable. ```nix gitClass = @@ -237,7 +304,7 @@ den.aspects.tux = { This will set at host: `home-manager.users.tux.programs.git.userEmail` -#### Example: A `nix` class that propagates settings to NixOS and HomeManager +### Example: A `nix` class that propagates settings to NixOS and HomeManager This can be used when you don't want NixOS and HomeManager to share the same pkgs but still configure both at the same time. @@ -265,7 +332,7 @@ nix-allowed = { user, ... }: { nix.allowed-users = [ user.userName ]; }; den.aspects.tux.includes = [ nix-allowed ]; ``` -#### Example: An impermanence class +### Example: An impermanence class > Suggested by @Doc-Steve diff --git a/modules/aspects/provides/forward.nix b/modules/aspects/provides/forward.nix index 64ac99663..21b27232b 100644 --- a/modules/aspects/provides/forward.nix +++ b/modules/aspects/provides/forward.nix @@ -50,13 +50,18 @@ let "adaptArgs" "adapterModule" ]; + item = lib.head fwd.each; fromClass = fwd.fromClass item; intoClass = fwd.intoClass item; intoPath = fwd.intoPath item; + + sourceModule = den.lib.aspects.resolve fromClass [ ] (fwd.fromAspect item); + freeformMod = { config._module.freeformType = lib.types.lazyAttrsOf lib.types.unspecified; }; + adapterKey = lib.concatStringsSep "/" ( [ fromClass @@ -101,10 +106,41 @@ let }; }; + topLevelAdapter.${intoClass} = { + __functionArgs = guardArgs; + __functor = + _: args: + let + extraArgs = + if adaptArgs == null then { } else builtins.removeAttrs (adaptArgs args) (builtins.attrNames args); + specialArgs = + builtins.removeAttrs args [ + "config" + "options" + "lib" + ] + // extraArgs; + evaluated = lib.evalModules { + inherit specialArgs; + modules = (if adapterModule == null then [ freeformMod ] else [ adapterModule ]) ++ [ + sourceModule + ]; + }; + in + guardFn args evaluated.config; + }; + needsAdapter = guard != null || adaptArgs != null || adapterModule != null; + needsTopLevelAdapter = needsAdapter && intoPath == [ ]; forwarded = den.lib.aspects.forward clean; + in - if needsAdapter then adapter else forwarded; + if needsTopLevelAdapter then + topLevelAdapter + else if needsAdapter then + adapter + else + forwarded; in { diff --git a/templates/ci/modules/features/forward-alias-class.nix b/templates/ci/modules/features/forward-alias-class.nix new file mode 100644 index 000000000..5d8e9734b --- /dev/null +++ b/templates/ci/modules/features/forward-alias-class.nix @@ -0,0 +1,154 @@ +{ denTest, ... }: +{ + flake.tests.forward-alias-class = { + + test-home-alias-forwards-into-home-manager-root = denTest ( + { + den, + lib, + igloo, + ... + }: + let + forwarded = + { class, aspect-chain }: + den._.forward { + each = lib.singleton class; + fromClass = _: "home"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + adaptArgs = + { config, ... }: + { + osConfig = config; + }; + }; + in + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + + den.aspects.igloo.nixos.networking.hostName = "storm"; + + den.aspects.tux = { + includes = [ forwarded ]; + home = + { osConfig, ... }: + { + programs.fish.enable = true; + home.keyboard.model = osConfig.networking.hostName; + }; + }; + + expr = { + enable = igloo.home-manager.users.tux.programs.fish.enable; + model = igloo.home-manager.users.tux.home.keyboard.model; + }; + expected = { + enable = true; + model = "storm"; + }; + } + ); + + test-guarded-home-alias-forwards-into-home-manager-root = denTest ( + { + den, + lib, + igloo, + ... + }: + let + forwarded = + { class, aspect-chain }: + den._.forward { + each = lib.singleton class; + fromClass = _: "home"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + guard = { config, ... }: _: lib.mkIf config.programs.fish.enable; + adaptArgs = + { config, ... }: + { + osConfig = config; + }; + }; + in + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + + den.aspects.igloo.nixos.networking.hostName = "storm"; + den.aspects.tux.homeManager.programs.fish.enable = true; + + den.aspects.tux = { + includes = [ forwarded ]; + home = + { osConfig, ... }: + { + home.keyboard.model = osConfig.networking.hostName; + }; + }; + + expr = { + enable = igloo.home-manager.users.tux.programs.fish.enable; + model = igloo.home-manager.users.tux.home.keyboard.model; + }; + expected = { + enable = true; + model = "storm"; + }; + } + ); + + test-hm-platforms-example = denTest ( + { + den, + lib, + igloo, + apple, + ... + }: + let + forwarded = + { class, aspect-chain }: + den._.forward { + each = [ + "Linux" + "Darwin" + ]; + fromClass = platform: "hm${platform}"; + intoClass = _: "homeManager"; + intoPath = _: [ ]; + fromAspect = _: lib.head aspect-chain; + guard = { pkgs, ... }: platform: lib.mkIf pkgs.stdenv."is${platform}"; + adaptArgs = + { config, ... }: + { + osConfig = config; + }; + }; + in + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + den.hosts.aarch64-darwin.apple.users.tux = { }; + + den.aspects.tux = { + includes = [ forwarded ]; + hmLinux.home.keyboard.model = "freedom"; + hmDarwin.home.keyboard.model = "closed"; + }; + + expr = { + linux = igloo.home-manager.users.tux.home.keyboard.model; + darwin = apple.home-manager.users.tux.home.keyboard.model; + }; + expected = { + linux = "freedom"; + darwin = "closed"; + }; + } + ); + + }; +}