home-manager-alias-namespace¶
Modules
A thin, top-level home.* namespace that any NixOS module can contribute to,
forwarded into one or more Home Manager users — so modules never have to know
which user owns the Home Manager config.
The problem¶
Home Manager config lives under home-manager.users.<name>.…. That name is a
per-host detail: the login user might be alice on one machine and bob on
another, and a single machine may run HM for several users at once. If every
module that wants to drop a dotfile has to spell out the username, you either
hardcode it everywhere (and break on the next host) or thread the username
through as an argument (and pollute every module signature).
The insight¶
Declare a host-neutral top-level namespace — home.file, home.activation,
home.env, home.programs, home.services — that modules write to freely:
Then alias those definitions into each real Home Manager user with
lib.mkAliasDefinitions:
mkAliasDefinitions forwards the definitions of one option onto another,
preserving each contributor's priority (mkForce, mkDefault, mkIf, …)
instead of collapsing everything to a single merged value. That means N modules
can each add to home.file and their overrides still resolve correctly on the
far side. The username is decided in exactly one place (this module's
users option); everything else stays generic.
Using lib.genAttrs users mkUser, the same aliased definitions land in every
managed user, so one home.file.… set anywhere applies to all of them.
What else it bundles¶
Two small conveniences that pair naturally with a workstation HM setup, both individually toggleable:
-
Nightly
nix-indexrebuild. Keeps thenix-locate/ command-not-found database fresh. It runs as aoneshotatNice 19withIOSchedulingClass = idle, so it never competes with foreground work, and the timer isPersistentwith a 30-minuteRandomizedDelaySecso a machine that was off at the scheduled time still catches up (and a fleet doesn't all fire at once). -
XDG user-dirs archive redirect. Points Desktop / Downloads / Pictures / Videos into an
archive/subtree to keep$HOMEuncluttered, while Documents / Music / Templates collapse back to$HOME.
Traps worth knowing¶
-
user-dirs.confwithenabled=False. Without it, thexdg-user-dirs-updatedaemon rewrites your carefully redirected paths back to defaults on the next login. This line disables that daemon so the declarative paths stick. -
createDirectories = false. HM would otherwise materialise emptyDesktop/,Templates/, etc. that you never asked for. -
nix-indexneedsHOMEset. The rebuild writes~/.cache/nix-index/files; a systemd service has noHOMEunless you set one, so thenixIndex.homeoption feeds it explicitly (defaults to/home/<user>). -
nix-indexwants the network. The rebuild fetches store metadata, hence thenetwork-online.targetordering. On an offline box the timer simply fails and retries next cycle. -
This module does not import home-manager. It assumes the Home Manager NixOS module is already imported by your configuration (via its flake input or channel). It only populates
home-manager.users.
Usage¶
Import default.nix, enable it, and name your users:
{
imports = [ ./home-manager-alias-namespace ];
enable-home-manager = true;
home-manager-alias = {
users = [ "alice" ]; # who receives the aliased home.* namespace
enableLorri = true; # optional: services.lorri.enable per user
xdg.enable = true; # optional: archive/ user-dirs redirect
xdg.archiveRoot = "$HOME/archive";
nixIndex.enable = true; # optional: nightly nix-index rebuild
sharedModules = [ ]; # optional: extra HM modules for every user
};
# ...and now, from ANY module:
home.file.".config/foo".text = "…";
}
Options¶
| Option | Default | Purpose |
|---|---|---|
enable-home-manager |
false |
Master switch for the whole module. |
home.{file,activation,env,programs,services} |
{} |
The namespace modules write to. env aliases to home.sessionVariables. |
home-manager-alias.users |
[ "user" ] |
Users that receive the aliased namespace. |
home-manager-alias.enableLorri |
false |
Turn on services.lorri per user. |
home-manager-alias.xdg.enable |
true |
Redirect XDG user-dirs into an archive subtree. |
home-manager-alias.xdg.archiveRoot |
"$HOME/archive" |
Base dir for the archived XDG dirs. |
home-manager-alias.sharedModules |
[ ] |
HM modules applied to every managed user. |
home-manager-alias.nixIndex.enable |
true |
Nightly nix-index rebuild timer. |
home-manager-alias.nixIndex.user |
first of users |
User the rebuild runs as. |
home-manager-alias.nixIndex.home |
/home/<user> |
HOME for the rebuild service. |
home-manager-alias.nixIndex.nixPath |
null |
Optional NIX_PATH override. |
Caveats¶
- The
home.programs/home.servicesnamespaces are intentionally loose (untyped attrs) so any module can contribute without importing this file. That means typos in contributed attribute names surface as Home Manager errors, not option errors — the tradeoff for decoupling. man.generateCaches = falseandssh.enableDefaultConfig = falseare set on every managed user to avoid slow man-cache builds and HM's opinionated default SSH config. Drop them frommkUserif you want HM's defaults.
Source¶
modules/home-manager-alias-namespace/default.nix
# home-manager-alias-namespace
#
# Exposes a thin top-level `home.*` namespace (file / activation / env /
# programs / services) that `mkAliasDefinitions` forwards into one or more
# Home Manager users. Any other NixOS module can then write
#
# home.file.".config/foo".text = "...";
#
# without knowing which user owns the Home Manager config or how it is wired.
# Also ships an optional nightly `nix-index` rebuild (so `nix-locate` /
# command-not-found stays fresh) at idle IO priority, and an optional XDG
# user-dirs redirect that tucks the standard dirs under an archive/ subtree.
#
# Drop it into your imports and set `enable-home-manager = true;`. Requires the
# home-manager NixOS module to be imported elsewhere in your configuration.
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.enable-home-manager;
hm = config.home-manager-alias;
mkOpt' =
type: default: description:
lib.mkOption { inherit type default description; };
# XDG user-dirs pointed at an archive/ subtree so $HOME itself stays tidy.
# The unusual bits: `user-dirs.conf` with enabled=False stops the
# xdg-user-dirs-update daemon from rewriting these paths at login, and
# createDirectories=false avoids materialising empty dirs you never use.
xdgConfig = {
enable = true;
configFile."user-dirs.conf".text = "enabled=False\n";
userDirs = {
enable = true;
createDirectories = false;
setSessionVariables = true;
desktop = "${hm.xdg.archiveRoot}/desktop";
download = "${hm.xdg.archiveRoot}/downloads";
documents = "$HOME";
music = "$HOME";
pictures = "${hm.xdg.archiveRoot}/pictures";
publicShare = "$HOME/.public";
templates = "$HOME";
videos = "${hm.xdg.archiveRoot}/videos";
};
};
# Per-user Home Manager config. Every user in `home-manager-alias.users`
# receives the SAME aliased definitions, so a single `home.file.…` set by
# any module lands in all of them. mkAliasDefinitions is the load-bearing
# trick: it forwards the *definitions* (not the merged value) of the
# top-level option into the HM option, preserving priorities / mkForce /
# mkIf from the contributing modules.
mkUser = _name: lib.mkMerge [
{
home = {
inherit (config.system) stateVersion;
enableNixpkgsReleaseCheck = false;
file = lib.mkAliasDefinitions options.home.file;
activation = lib.mkAliasDefinitions options.home.activation;
sessionVariables = lib.mkAliasDefinitions options.home.env;
};
programs = (lib.mkAliasDefinitions options.home.programs) // {
man.generateCaches = false;
ssh.enableDefaultConfig = false;
};
services = (lib.mkAliasDefinitions options.home.services) // lib.optionalAttrs hm.enableLorri {
lorri.enable = true;
};
}
(lib.mkIf hm.xdg.enable { xdg = xdgConfig; })
];
in
{
options = {
enable-home-manager = lib.mkEnableOption "the top-level home.* alias namespace";
# The public namespace other modules contribute to. Kept deliberately
# loose (attrs) so any module can add to it without importing this file.
home = {
programs = lib.mkOption {
description = "Home Manager programs.* (contributed by other modules).";
default = { };
};
services = lib.mkOption {
description = "Home Manager services.* (contributed by other modules).";
default = { };
};
file = mkOpt' lib.types.attrs { } "Files to place directly in $HOME.";
activation = mkOpt' lib.types.attrs { } "Activation scripts to run on home-manager switch.";
env = mkOpt' lib.types.attrs { } "Environment variables to set on shells (aliased to home.sessionVariables).";
};
home-manager-alias = {
users = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "user" ];
example = [ "alice" "bob" ];
description = ''
Users whose Home Manager config receives the aliased home.*
namespace. Every listed user gets the same definitions.
'';
};
enableLorri = lib.mkEnableOption "the lorri services.lorri.enable for each user";
xdg = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Redirect XDG user-dirs into an archive/ subtree.";
};
archiveRoot = lib.mkOption {
type = lib.types.str;
default = "$HOME/archive";
description = "Base directory the archived XDG dirs live under.";
};
};
# Extra Home Manager modules shared across all managed users, e.g. a
# theming module. Left empty by default so this recipe carries no
# opinion about your desktop.
sharedModules = lib.mkOption {
type = lib.types.listOf lib.types.unspecified;
default = [ ];
description = "Home Manager modules applied to every managed user.";
};
nixIndex = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Install a nightly nix-index rebuild timer.";
};
user = lib.mkOption {
type = lib.types.str;
default = lib.head hm.users;
defaultText = lib.literalExpression "builtins.head config.home-manager-alias.users";
description = "User the nix-index rebuild runs as (its ~/.cache is written).";
};
home = lib.mkOption {
type = lib.types.str;
default = "/home/${hm.nixIndex.user}";
defaultText = lib.literalExpression ''"/home/''${config.home-manager-alias.nixIndex.user}"'';
description = "HOME for the rebuild service (nix-index writes ~/.cache/nix-index there).";
};
nixPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos";
description = "Optional NIX_PATH override for the rebuild (null = inherit system default).";
};
};
};
};
config = lib.mkIf cfg {
# Nightly nix-index rebuild. Runs at Nice 19 / idle IO so it never
# competes with foreground work; Persistent + RandomizedDelaySec means a
# box that was asleep at the scheduled time still catches up (jittered so
# a fleet doesn't stampede). Needs network for the store metadata fetch.
systemd.services.nix-index-update = lib.mkIf hm.nixIndex.enable {
description = "Rebuild the nix-index files database (nix-locate / command-not-found)";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = [
config.nix.package
pkgs.nix-index
];
environment = {
HOME = hm.nixIndex.home;
} // lib.optionalAttrs (hm.nixIndex.nixPath != null) {
NIX_PATH = hm.nixIndex.nixPath;
};
serviceConfig = {
Type = "oneshot";
User = hm.nixIndex.user;
ExecStart = "${pkgs.nix-index}/bin/nix-index";
Nice = 19;
IOSchedulingClass = "idle";
};
};
systemd.timers.nix-index-update = lib.mkIf hm.nixIndex.enable {
description = "Nightly rebuild of the nix-index files database";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = "30m";
Unit = "nix-index-update.service";
};
};
home-manager = {
backupFileExtension = ".bak";
useGlobalPkgs = true;
useUserPackages = true;
sharedModules = hm.sharedModules;
users = lib.genAttrs hm.users mkUser;
};
};
}