{ config, lib, ... }: let mkHostUser = { account, needsPassword ? false, homeImports ? [ ], stateVersion ? null, }: { inherit account homeImports needsPassword stateVersion ; }; mkHost = { name, displays ? { }, input ? { }, sourceControl ? { }, users ? { }, imports ? [ ], stateVersion ? "24.05", }: { config, pkgs, ... }: let hostUserSpecs = lib.mapAttrs (_: spec: mkHostUser spec) users; hostUsers = lib.mapAttrs (_: spec: spec.account) hostUserSpecs; userAssertions = lib.mapAttrsToList (userName: spec: { assertion = userName == spec.account.name; message = "Host `${name}` declares user `${userName}` with mismatched account name `${spec.account.name}`."; }) hostUserSpecs; passwordUserSpecs = lib.filterAttrs (_: spec: spec.needsPassword) hostUserSpecs; in { assertions = userAssertions; meta.host = { inherit displays input name sourceControl ; 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 { hashedPasswordFile = config.sops.secrets."hashed-password-${userName}".path; } ) hostUserSpecs; 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 = if spec.stateVersion == null then stateVersion else spec.stateVersion; }; } ) hostUserSpecs; }; mkCaddyReverseProxy = { domain, port, extraHeaders ? [ ], extraConfigText ? "", }: let headerLines = map (header: " header_up ${header.name} ${header.value}") extraHeaders; extraConfigLines = map (line: " ${line}") ( lib.filter (line: line != "") (lib.splitString "\n" extraConfigText) ); bodyLines = headerLines ++ extraConfigLines; body = lib.concatStringsSep "\n" bodyLines; in { services.caddy.virtualHosts.${domain}.extraConfig = if body == "" then "reverse_proxy :${toString port}" else '' reverse_proxy :${toString port} { ${body} } ''; }; resolvePackagePath = { pkgs, path, }: lib.attrByPath path null pkgs; 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 = { displayName = "Kanagawa Wave"; name = "kanagawa-wave"; 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"; palette = { background = "#1F1F28"; foreground = "#DCD7BA"; secondaryBackground = "#16161D"; border = "#2A2A37"; selectionBackground = "#2D4F67"; selectionForeground = "#C8C093"; url = "#72A7BC"; cursor = "#C8C093"; muted = "#727169"; accents = { blue = "#7E9CD8"; green = "#98BB6C"; magenta = "#D27E99"; orange = "#FFA066"; purple = "#957FB8"; red = "#E82424"; yellow = "#E6C384"; cyan = "#7AA89F"; }; niri.border = { active = "#7E9CD8"; inactive = "#54546D"; urgent = "#E82424"; }; terminal = { color0 = "#16161D"; color1 = "#C34043"; color2 = "#76946A"; color3 = "#C0A36E"; color4 = "#7E9CD8"; color5 = "#957FB8"; color6 = "#6A9589"; color7 = "#C8C093"; color8 = "#727169"; color9 = "#E82424"; color10 = "#98BB6C"; color11 = "#E6C384"; color12 = "#7FB4CA"; color13 = "#938AA9"; color14 = "#7AA89F"; color15 = "#DCD7BA"; color16 = "#FFA066"; color17 = "#FF5D62"; }; }; }; }; }; resolveUserTerminal = { pkgs, user, }: let 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 { 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 = 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."; }) ]; 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 ); 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"; }; }; 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; }; }; }; 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" ]; }; }; 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 { options.meta.lib.mkHost = lib.mkOption { type = lib.types.raw; description = "Internal host constructor shared between flake-parts modules."; internal = true; readOnly = true; }; options.meta.lib.mkHostUser = lib.mkOption { type = lib.types.raw; description = "Internal host user constructor shared between flake-parts modules."; internal = true; readOnly = true; }; options.meta.lib.mkCaddyReverseProxy = lib.mkOption { type = lib.types.raw; description = "Internal Caddy reverse proxy helper shared between flake-parts modules."; internal = true; readOnly = true; }; options.meta.lib.resolvePackagePath = lib.mkOption { type = lib.types.raw; description = "Internal helper to resolve package attr paths against the local pkgs set."; internal = true; readOnly = true; }; options.meta.lib.resolveUserTerminal = lib.mkOption { type = lib.types.raw; 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; }; options.meta.lib.users = lib.mkOption { type = lib.types.attrs; description = "Canonical user attrsets shared by host definitions."; internal = true; 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 ; }; }