refactor: simplify module composition
This commit is contained in:
+330
-45
@@ -15,20 +15,84 @@ let
|
||||
stateVersion ? "24.05",
|
||||
}:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
hostUsers = lib.mapAttrs (_: spec: spec.account) users;
|
||||
|
||||
userAssertions = lib.flatten (
|
||||
lib.mapAttrsToList (userName: spec: [
|
||||
{
|
||||
assertion = userName == spec.account.name;
|
||||
message = "Host `${name}` declares user `${userName}` with mismatched account name `${spec.account.name}`.";
|
||||
}
|
||||
{
|
||||
assertion = builtins.isList spec.homeImports;
|
||||
message = "Host `${name}` user `${userName}` must define `homeImports` as a list.";
|
||||
}
|
||||
]) users
|
||||
);
|
||||
|
||||
passwordUserSpecs = lib.filterAttrs (_: spec: spec.needsPassword or false) users;
|
||||
in
|
||||
{
|
||||
assertions = userAssertions;
|
||||
|
||||
meta.host = {
|
||||
inherit
|
||||
displays
|
||||
input
|
||||
name
|
||||
sourceControl
|
||||
users
|
||||
;
|
||||
users = hostUsers;
|
||||
};
|
||||
|
||||
inherit imports;
|
||||
|
||||
networking.hostName = name;
|
||||
system.stateVersion = stateVersion;
|
||||
|
||||
programs.zsh.enable = true;
|
||||
|
||||
sops.secrets = lib.mapAttrs' (
|
||||
userName: _:
|
||||
lib.nameValuePair "hashed-password-${userName}" {
|
||||
neededForUsers = true;
|
||||
}
|
||||
) passwordUserSpecs;
|
||||
|
||||
users.users = lib.mapAttrs (
|
||||
userName: spec:
|
||||
{
|
||||
name = spec.account.name;
|
||||
home = spec.account.homeDirectory;
|
||||
isNormalUser = true;
|
||||
shell = pkgs.zsh;
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"networkmanager"
|
||||
];
|
||||
}
|
||||
// lib.optionalAttrs (spec.needsPassword or false) {
|
||||
hashedPasswordFile = config.sops.secrets."hashed-password-${userName}".path;
|
||||
}
|
||||
) users;
|
||||
|
||||
home-manager.users = lib.mapAttrs (_: spec: {
|
||||
imports = spec.homeImports;
|
||||
meta = {
|
||||
host = config.meta.host;
|
||||
user = spec.account;
|
||||
};
|
||||
home = {
|
||||
username = spec.account.name;
|
||||
homeDirectory = spec.account.homeDirectory;
|
||||
stateVersion = spec.stateVersion or stateVersion;
|
||||
};
|
||||
}) users;
|
||||
};
|
||||
|
||||
mkCaddyReverseProxy =
|
||||
@@ -65,62 +129,259 @@ let
|
||||
}:
|
||||
lib.attrByPath path null pkgs;
|
||||
|
||||
mkHostUser =
|
||||
repo = {
|
||||
contact.email = "mail@jelles.net";
|
||||
|
||||
services = {
|
||||
actual = {
|
||||
domain = "finance.jelles.net";
|
||||
host = "127.0.0.1";
|
||||
port = 3000;
|
||||
url = "https://finance.jelles.net";
|
||||
};
|
||||
|
||||
gitea = {
|
||||
domain = "git.jelles.net";
|
||||
host = "127.0.0.1";
|
||||
port = 3001;
|
||||
url = "https://git.jelles.net/";
|
||||
};
|
||||
|
||||
radicale = {
|
||||
domain = "radicale.jelles.net";
|
||||
host = "127.0.0.1";
|
||||
port = 5232;
|
||||
url = "https://radicale.jelles.net/";
|
||||
};
|
||||
|
||||
vaultwarden = {
|
||||
domain = "vault.jelles.net";
|
||||
host = "127.0.0.1";
|
||||
port = 8100;
|
||||
url = "https://vault.jelles.net";
|
||||
};
|
||||
};
|
||||
|
||||
theme = {
|
||||
cursor = {
|
||||
name = "phinger-cursors-light";
|
||||
packagePath = [ "phinger-cursors" ];
|
||||
size = 24;
|
||||
};
|
||||
|
||||
kanagawa = {
|
||||
gtkThemeName = "Kanagawa-BL-LB";
|
||||
iconThemeName = "Kanagawa";
|
||||
owner = "Fausto-Korpsvart";
|
||||
repo = "Kanagawa-GKT-Theme";
|
||||
rev = "55ca4ba249eba21f861b9866b71ab41bb8930318";
|
||||
hash = "sha256-UdMoMx2DoovcxSp/zBZ3PRv/Qpj+prd0uPm1gmdak2E=";
|
||||
version = "unstable-2025-10-23";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
resolveUserTerminal =
|
||||
{
|
||||
account,
|
||||
homeImports,
|
||||
needsPassword ? false,
|
||||
stateVersion ? "24.05",
|
||||
}:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
user,
|
||||
}:
|
||||
let
|
||||
name = account.name;
|
||||
primaryEmails = lib.filter (email: email.primary) (builtins.attrValues account.emails);
|
||||
package = resolvePackagePath {
|
||||
inherit pkgs;
|
||||
path = user.terminalPackagePath;
|
||||
};
|
||||
hasPackage = package != null;
|
||||
hasMainProgram = hasPackage && package ? meta.mainProgram;
|
||||
mainProgram = if hasMainProgram then package.meta.mainProgram else null;
|
||||
desktopId = if mainProgram == null then null else "${mainProgram}.desktop";
|
||||
in
|
||||
{
|
||||
assertions = [
|
||||
inherit
|
||||
desktopId
|
||||
hasMainProgram
|
||||
hasPackage
|
||||
mainProgram
|
||||
package
|
||||
;
|
||||
|
||||
hasDesktopEntry =
|
||||
desktopId != null && builtins.pathExists "${package}/share/applications/${desktopId}";
|
||||
|
||||
hasTerminfo = hasPackage && lib.elem "terminfo" package.outputs;
|
||||
};
|
||||
|
||||
mkTerminalAssertions =
|
||||
{
|
||||
terminal,
|
||||
user,
|
||||
requireDesktopEntry ? false,
|
||||
requireKitty ? false,
|
||||
requireMainProgram ? true,
|
||||
requireTerminfo ? false,
|
||||
}:
|
||||
lib.flatten [
|
||||
[
|
||||
{
|
||||
assertion = builtins.length primaryEmails == 1;
|
||||
message = "User ${name} must define exactly one primary email entry.";
|
||||
assertion = terminal.hasPackage;
|
||||
message = "Unknown terminal package `${lib.showAttrPath user.terminalPackagePath}` for user `${user.name}`.";
|
||||
}
|
||||
];
|
||||
]
|
||||
(lib.optional requireMainProgram {
|
||||
assertion = terminal.hasMainProgram;
|
||||
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must define `meta.mainProgram`.";
|
||||
})
|
||||
(lib.optional requireDesktopEntry {
|
||||
assertion = terminal.hasDesktopEntry;
|
||||
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must provide `${terminal.desktopId}`.";
|
||||
})
|
||||
(lib.optional requireKitty {
|
||||
assertion = terminal.hasMainProgram && terminal.mainProgram == "kitty";
|
||||
message = "The terminal feature currently only supports kitty-specific Home Manager configuration.";
|
||||
})
|
||||
(lib.optional requireTerminfo {
|
||||
assertion = terminal.hasTerminfo;
|
||||
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must provide a `terminfo` output.";
|
||||
})
|
||||
];
|
||||
|
||||
programs.zsh.enable = true;
|
||||
mapConfiguredAttrs =
|
||||
{
|
||||
settings,
|
||||
schema,
|
||||
}:
|
||||
lib.filterAttrs (_: value: value != null) (
|
||||
lib.mapAttrs (
|
||||
_: spec:
|
||||
let
|
||||
value = lib.getAttrFromPath spec.path settings;
|
||||
in
|
||||
if value == null then
|
||||
null
|
||||
else if spec ? values then
|
||||
spec.values.${value}
|
||||
else if spec ? transform then
|
||||
spec.transform value
|
||||
else
|
||||
value
|
||||
) schema
|
||||
);
|
||||
|
||||
sops.secrets = lib.optionalAttrs needsPassword {
|
||||
"hashed-password-${name}".neededForUsers = true;
|
||||
mkInputProfiles =
|
||||
input:
|
||||
let
|
||||
scrollMethodValues = {
|
||||
edge = {
|
||||
libinput = "edge";
|
||||
niri = "edge";
|
||||
};
|
||||
"no-scroll" = {
|
||||
libinput = "none";
|
||||
niri = "no-scroll";
|
||||
};
|
||||
"on-button-down" = {
|
||||
libinput = "button";
|
||||
niri = "on-button-down";
|
||||
};
|
||||
"two-finger" = {
|
||||
libinput = "twofinger";
|
||||
niri = "two-finger";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.${name} = {
|
||||
name = account.name;
|
||||
home = account.homeDirectory;
|
||||
isNormalUser = true;
|
||||
shell = pkgs.zsh;
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"networkmanager"
|
||||
];
|
||||
}
|
||||
// lib.optionalAttrs needsPassword {
|
||||
hashedPasswordFile = config.sops.secrets."hashed-password-${name}".path;
|
||||
mouse = input.mouse;
|
||||
touchpad = input.touchpad;
|
||||
|
||||
libinputMouse = mapConfiguredAttrs {
|
||||
settings = mouse;
|
||||
schema = {
|
||||
accelProfile.path = [ "accelProfile" ];
|
||||
accelSpeed = {
|
||||
path = [ "accelSpeed" ];
|
||||
transform = toString;
|
||||
};
|
||||
leftHanded.path = [ "leftHanded" ];
|
||||
middleEmulation.path = [ "middleEmulation" ];
|
||||
naturalScrolling.path = [ "naturalScrolling" ];
|
||||
scrollMethod = {
|
||||
path = [ "scrollMethod" ];
|
||||
values = lib.mapAttrs (_: value: value.libinput) scrollMethodValues;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.${name} = {
|
||||
imports = homeImports;
|
||||
meta = {
|
||||
host = config.meta.host;
|
||||
user = account;
|
||||
libinputTouchpad = mapConfiguredAttrs {
|
||||
settings = touchpad;
|
||||
schema = {
|
||||
accelProfile.path = [ "accelProfile" ];
|
||||
accelSpeed = {
|
||||
path = [ "accelSpeed" ];
|
||||
transform = toString;
|
||||
};
|
||||
clickMethod = {
|
||||
path = [ "clickMethod" ];
|
||||
values = {
|
||||
"button-areas" = "buttonareas";
|
||||
clickfinger = "clickfinger";
|
||||
};
|
||||
};
|
||||
disableWhileTyping.path = [ "disableWhileTyping" ];
|
||||
leftHanded.path = [ "leftHanded" ];
|
||||
middleEmulation.path = [ "middleEmulation" ];
|
||||
naturalScrolling.path = [ "naturalScrolling" ];
|
||||
scrollMethod = {
|
||||
path = [ "scrollMethod" ];
|
||||
values = lib.mapAttrs (_: value: value.libinput) scrollMethodValues;
|
||||
};
|
||||
tapping.path = [ "tapping" ];
|
||||
};
|
||||
home = {
|
||||
username = account.name;
|
||||
homeDirectory = account.homeDirectory;
|
||||
inherit stateVersion;
|
||||
};
|
||||
|
||||
niriMouse = mapConfiguredAttrs {
|
||||
settings = mouse;
|
||||
schema = {
|
||||
"accel-profile".path = [ "accelProfile" ];
|
||||
"accel-speed".path = [ "accelSpeed" ];
|
||||
"left-handed".path = [ "leftHanded" ];
|
||||
"middle-emulation".path = [ "middleEmulation" ];
|
||||
"natural-scroll".path = [ "naturalScrolling" ];
|
||||
"scroll-method" = {
|
||||
path = [ "scrollMethod" ];
|
||||
values = lib.mapAttrs (_: value: value.niri) scrollMethodValues;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
niriTouchpad = mapConfiguredAttrs {
|
||||
settings = touchpad;
|
||||
schema = {
|
||||
"accel-profile".path = [ "accelProfile" ];
|
||||
"accel-speed".path = [ "accelSpeed" ];
|
||||
"click-method".path = [ "clickMethod" ];
|
||||
dwt.path = [ "disableWhileTyping" ];
|
||||
"left-handed".path = [ "leftHanded" ];
|
||||
"middle-emulation".path = [ "middleEmulation" ];
|
||||
"natural-scroll".path = [ "naturalScrolling" ];
|
||||
"scroll-method" = {
|
||||
path = [ "scrollMethod" ];
|
||||
values = lib.mapAttrs (_: value: value.niri) scrollMethodValues;
|
||||
};
|
||||
tap.path = [ "tapping" ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
hasPointerConfig = libinputMouse != { } || libinputTouchpad != { };
|
||||
|
||||
libinput = {
|
||||
mouse = libinputMouse;
|
||||
touchpad = libinputTouchpad;
|
||||
};
|
||||
|
||||
niri = {
|
||||
mouse = niriMouse;
|
||||
touchpad = niriTouchpad;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
@@ -138,16 +399,30 @@ in
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
options.meta.lib.mkHostUser = lib.mkOption {
|
||||
options.meta.lib.resolvePackagePath = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "Internal helper for explicit per-host user assembly.";
|
||||
description = "Internal helper to resolve package attr paths against the local pkgs set.";
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
options.meta.lib.resolvePackagePath = lib.mkOption {
|
||||
options.meta.lib.resolveUserTerminal = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "Internal helper to resolve package attr paths against the local pkgs set.";
|
||||
description = "Internal helper to resolve and validate user terminal metadata.";
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
options.meta.lib.mkTerminalAssertions = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "Internal helper for terminal-related assertions shared across modules.";
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
options.meta.lib.mkInputProfiles = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = "Internal helper to normalize host input metadata for libinput and Niri.";
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
};
|
||||
@@ -159,12 +434,22 @@ in
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
options.meta.lib.repo = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
description = "Internal shared repository metadata.";
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
config.meta.lib = {
|
||||
inherit
|
||||
mkInputProfiles
|
||||
mkCaddyReverseProxy
|
||||
mkTerminalAssertions
|
||||
mkHost
|
||||
mkHostUser
|
||||
repo
|
||||
resolvePackagePath
|
||||
resolveUserTerminal
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user