Skip to content

Custom Nix Classes

Den’s built-in classes (nixos, darwin, homeManager) map to well-known NixOS module systems. But you can define custom classes that forward their contents into a target submodule path on another class.

This is how Den implements:

  • The user class (forwards to users.users.<name> on the OS)
  • Home Manager integration (forwards homeManager to home-manager.users.<name>)
  • hjem integration (forwards hjem to hjem.users.<name>)
  • nix-maid integration (forwards maid to users.users.<name>.maid)

den.provides.forward creates a new class by forwarding its module contents into a target path on an existing class:

{ host }:
den.provides.forward {
each = lib.attrValues host.users;
fromClass = user: "user";
intoClass = user: host.class;
intoPath = user: [ "users" "users" user.userName ];
fromAspect = user: den.aspects.${user.aspect};
}
ParameterDescription
each = itemsList of items to forward (typically [ user ] or [ true ])
fromClass = item: classThe custom class name to read from
intoClass = item: classThe target class to write into
intoPath = item: path or args: pathTarget attribute path in the target class
fromAspect = item: aspectThe aspect to read the custom class from

The user class (provides/os-user.nix) forwards OS-level user settings to NixOS/nix-Darwin lightweight user-environment.

# Instead of:
den.aspects.alice.nixos = { pkgs, ... } {
users.users.alice = {
packages = [ pkgs.hello ];
extraGroups = [ "wheel" ];
};
};
# You write:
den.aspects.alice.user = { pkgs, ... }: {
packages = [ pkgs.hello ];
extraGroups = [ "wheel" ];
};

The user class is automatically forwarded to users.users.<userName> on whatever OS class the host uses (NixOS or Darwin).

Suppose you want a container class that forwards into virtualisation.oci-containers.containers.<name>:

{ den, lib, ... }:
let
fwd = { host, user }:
den.provides.forward {
each = lib.singleton user;
fromClass = _: "container";
intoClass = _: host.class;
intoPath = _: [ "virtualisation" "oci-containers" "containers" user.userName ];
fromAspect = _: den.aspects.${user.aspect};
};
in {
den.ctx.user.includes = [ fwd ];
}

Now any user aspect can use the container class:

den.aspects.alice.container = {
image = "nginx:latest";
ports = [ "8080:80" ];
};

forward supports optional parameters for complex scenarios:

den.provides.forward {
each = lib.singleton true; # <- true is the single item, mostly ignored.
fromClass = _item: "wsl";
intoClass = _item: host.class;
intoPath = _item: [ "wsl" ];
fromAspect = _item: lib.head aspect-chain;
# Only forward if target has wsl options
guard = { options, ... }: options ? wsl;
# Modify module args for the forwarded module
adaptArgs = args: { osConfig = args.config; };
# Custom module type for the forwarded submodule
adapterModule = { config._module.freeformType = lib.types.anything; };
}
ParameterDescription
guard = args: bool or item: mkIfOnly forward when this predicate returns true
adaptArgs = args: attrsTransform module arguments before forwarding
adapterModule = deferredModuleCustom module for the forwarded submodule type

Example: Alias a Class into the Target Root

Section titled “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.

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:

This pattern is useful when you need HM to distinguish between different OS Platforms, because some packages only build in Darwin and not NixOS.

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.

Section titled “Example: Config across nixos and darwin classes.”

The os forward class (provided by Den) can be useful for settings that must be forwarded to both on NixOS and MacOS.

Requested by @Risa-G at #222

# Note: this is already provided by Den at provides/os-class.nix
os-class = { class, aspect-chain }: den._.forward {
each = [ "nixos" "darwin" ];
fromClass = _: "os";
intoClass = lib.id;
intoPath = _: [ ]; # top-level
fromAspect = _: lib.head aspect-chain;
};
# Note: already enabled by Den
# den.ctx.host.includes = [ os-class ];
den.aspects.my-laptop = {
os.networking.hostName = "Yavanna"; # on both NixOS and MacOS
};

Example: Role based configuration between users and hosts

Section titled “Example: Role based configuration between users and hosts”

A dynamic class for matching roles between users and hosts.

roleClass =
{ host, user }:
{ class, aspect-chain }:
den._.forward {
each = lib.intersectLists (host.roles or []) (user.roles or []);
fromClass = lib.id;
intoClass = _: host.class;
intoPath = _: [ ];
fromAspect = _: lib.head aspect-chain;
};
den.ctx.user.includes = [ roleClass ];
den.hosts.x86_64-linux.igloo = {
roles = [ "devops" "gaming" ];
users = {
alice.roles = [ "gaming" ];
bob.roles = [ "devops" ];
};
};
den.aspects.alice = {
# enabled when host supports gaming role
gaming = { pkgs, ... }: { programs.steam.enable = true; };
# enabled when host supports devops role
devops = { pkgs, ... }: { virtualisation.podman.enable = true; };
};
gitClass =
{ class, aspect-chain }:
den._.forward {
each = lib.singleton true;
fromClass = _: "git";
intoClass = _: "homeManager";
intoPath = _: [ "programs" "git" ];
fromAspect = _: lib.head aspect-chain;
guard = { config, ... }: _: lib.mkIf config.programs.git.enable;
};
den.aspects.tux = {
includes = [ gitClass ];
git.userEmail = "root@linux.com";
};

This will set at host: home-manager.users.tux.programs.git.userEmail

Example: A nix class that propagates settings to NixOS and HomeManager

Section titled “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.

Contributed by @musjj

nixClass =
{ class, aspect-chain }:
den._.forward {
each = [ "nixos" "homeManager" ];
fromClass = _: "nix";
intoClass = lib.id;
intoPath = _: [ "nix" "settings" ];
fromAspect = _: lib.head aspect-chain;
adaptArgs = lib.id;
};
# enable class for all users:
den.ctx.user.includes = [ nixClass ];
# custom aspect that uses the `nix` class.
nix-allowed = { user, ... }: { nix.allowed-users = [ user.userName ]; };
# included at users who can fix things with nix.
den.aspects.tux.includes = [ nix-allowed ];

Suggested by @Doc-Steve

The following example, creates a forwarding class that is propagated only when environment.persistance option is available in the host (the impermanence module was imported in host)

One cool thing about these custom classes is that aspects can simply define settings at the new class, without having to worry if the options they depend or some capability is enabled.

The froward-guard itself is reponsible checking in only one place, instead of having mkIf in a lot of places.

# Custom `persys` forwards config into nixos.environment.persistance."/nix/persist/system"
# only if environment.persistance option is present.
persys = { class, aspect-chain }: den._.forward {
each = lib.singleton true;
fromClass = _item: "persys";
intoClass = _item: class;
intoPath = _item:
{ config, ... }: # access intoClass module arguments:
[ "environment" "persistance" config.impermanence.persistence-dir ];
fromAspect = _item: lib.head aspect-chain;
guard = { options, ... }@osArgs: options ? environment.persistance;
};
den.hosts.my-laptop.includes = [ persys ];
# becomes nixos.environment.persistance."/nix/persist/system".hideMounts = true;
den.aspects.my-laptop.persys.hideMounts = true;
Contribute Community Sponsor