System and home in one feature
A feature such as a desktop often has two parts: system configuration, and home-manager configuration for each user. The usual answers each have a cost:
home-manager.sharedModulesin the NixOS module. It does not work for standalone home-manager, and a module that is imported from two features is added two times.extraSpecialArgsto give home-manager the values that NixOS gets fromspecialArgs. You write them two times, and standalone home-manager needs a third copy.osConfig ? nullandlib.mkIf (osConfig != null), becauseosConfigdoes not exist in standalone mode.lib.mkIf pkgs.stdenv.isLinuxto send a package list to the system on one machine and to the home on another. AmkIfaroundimportscauses infinite recursion.
In yen, the feature is aspects. at says where each part goes. A user’s home-manager content goes into each
NixOS host that the user lives on. A standalone home builds its own homeConfigurations output.
# NixOS hosts hold users. A home is a standalone home-manager configuration, for a machine without NixOS.{ yen, inputs, ...}:{ yen.kinds.fleet = { spawns = { host = _: yen.host; home = _: yen.home; }; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.host = { spawns.user = yen.lib.pointing "hosts" yen.user; sinks.nixos.out = { host, nixos, ... }: { nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; }; }; }; yen.kinds.user = { fields.hosts = yen.lib.listOf yen.kinds.host; # a user's home-manager content goes into each NixOS host it lives on sinks.homeManager.nixos = # (1) { user, homeManager, ... }: { home-manager.users.${user.name}.imports = homeManager; }; }; yen.kinds.home = { fields = { user = yen.kinds.user; system = yen.lib.str; }; sinks.homeManager.out = # (2) { home, homeManager, ... }: { homeConfigurations.${home.name} = inputs.home-manager.lib.homeManagerConfiguration { pkgs = inputs.nixpkgs.legacyPackages.${home.system}; modules = homeManager; }; }; }; yen.aspects.home-manager = { at = yen.lib.below yen.kinds.user; nixos = { imports = [ inputs.home-manager.nixosModules.home-manager ]; home-manager.useGlobalPkgs = true; }; };
yen.fleet.main = { }; yen.flake = { out, ... }: out;}# ana has a NixOS desktop, and a work laptop with another Linux: there she has home-manager only.{ yen, ... }:{ yen.host.desk = { }; yen.user.ana.hosts = [ yen.host.desk ]; yen.home."ana@work" = { # (1) user = yen.user.ana; system = "x86_64-linux"; };
yen.aspects.desk = { at = yen.host.desk; nixos = { networking.hostName = "desk"; boot.loader.grub.device = "nodev"; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "25.05"; users.users.ana.isNormalUser = true; }; }; yen.aspects.home-base = { at = [ yen.kinds.user yen.kinds.home ]; homeManager.home.stateVersion = "25.05"; }; yen.aspects.standalone = { # (2) at = yen.kinds.home; homeManager = { home, ... }: { home.username = home.user.name; home.homeDirectory = "/home/${home.user.name}"; }; };}In fleet.nix, (1) is the homeManager sink of a user: it forwards the content to the
nixos sink of each host the user lives on (forwards). (2) is the homeManager sink
of a standalone home: it builds the homeConfigurations output. In machines.nix, (1) is the home ana@work
of the user ana, and (2) is the aspect that every home gets.
Two levels: the desktop and its users
Section titled “Two levels: the desktop and its users”This is the multi context aspect of the dendritic design guide. The home part is not a private module that the system part imports. It is an aspect at the users of every host that has the desktop. Two features that both need it do not import it two times: an aspect is given once per scope.
# One feature, two levels. The desktop is a system concern; its settings are a home concern of each user on it.{ yen, ... }:let inherit (yen.lib) all within;in{ yen.aspects.gnome = { at = yen.host.desk; # (1) nixos.services.xserver.desktopManager.gnome.enable = true; }; # no home-manager.sharedModules: the users of a host that has gnome get this yen.aspects.gnome-home = { at = all [ yen.kinds.user (within yen.aspects.gnome) # (2) ]; homeManager.dconf.settings."org/gnome/desktop/interface".color-scheme = "prefer-dark"; };}(1) is the system part: it is active at the host desk. (2) makes the home part active at every user
within a host where gnome is active (predicates).
The same concern, a different level per machine
Section titled “The same concern, a different level per machine”On NixOS, fonts are system packages. On the work laptop there is no NixOS, so they go into the home. The two
aspects have different at: (1) is every host, and (2) is every home. No module looks at pkgs.stdenv
to find out where it is.
# Fonts are a system concern on NixOS, and a home concern where there is no NixOS. `at` decides, not mkIf.{ yen, ... }:{ yen.aspects.fonts-system = { at = yen.kinds.host; # (1) nixos = { pkgs, ... }: { fonts.packages = [ pkgs.inter ]; }; }; yen.aspects.fonts-home = { at = yen.kinds.home; # (2) homeManager = { pkgs, ... }: { home.packages = [ pkgs.inter ]; fonts.fontconfig.enable = true; }; };}Values for home-manager
Section titled “Values for home-manager”A home-manager module gets the same yen args as a NixOS module: the host, the user, and any
sink. There is no extraSpecialArgs. To read the host’s NixOS options from home-manager, see
osConfig in home-manager.
# A home-manager module reads its host and its user as args: no extraSpecialArgs, no osConfig.{ yen, ... }:{ yen.aspects.prompt = { at = yen.lib.all [ yen.kinds.user (yen.lib.within yen.kinds.host) # (1) ]; homeManager = { host, user, ... }: # (2) { home.sessionVariables.PROMPT_TAG = "${user.name}@${host.name}"; }; };}(1) makes the aspect active at every user within a host. (2) asks for the host and the user.