diff --git a/AGENTS.md b/AGENTS.md index 29b035b..de609cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,8 +35,8 @@ In this repo, `flake.nix` imports `./modules` recursively via `inputs.import-tre - `modules/hosts//default.nix`: host features that assemble NixOS aspects into `flake.modules.nixos.`. - `modules/secrets/`: secret-related features shared by hosts. - `modules/flake-parts.nix`: flake-parts entrypoint; defines systems, formatter, and `flake.nixosConfigurations`. -- `modules/lib.nix`: shared constructors in `config.meta.lib`, especially `mkHost`, `mkHostUser`, and `mkCaddyReverseProxy`. -- `modules/users.nix`: canonical user attrsets exposed through `meta.lib.users`. +- `modules/lib.nix`: shared constructors and helpers in `config.meta.lib`, especially `mkHost` and `mkCaddyReverseProxy`. +- `modules/data.nix`: canonical shared repo data and account attrsets exposed through `meta.lib.repo` and `meta.lib.accounts`. - `modules/features/meta.nix`: shared metadata schema for `meta.host` and `meta.user`. ## How Features Are Applied Here @@ -45,7 +45,7 @@ In this repo, `flake.nix` imports `./modules` recursively via `inputs.import-tre - Reusable Home Manager concerns are published as `flake.modules.homeManager.`. - Hosts are aspects too. `orion`, `polaris`, and `zenith` are `nixos` aspects assembled from smaller aspects. - Host modules should use `config.meta.lib.mkHost` to define `meta.host`, base imports, hostname, and state version. -- Per-host user declarations should use `config.meta.lib.mkHostUser` so host-local defaults stay close to the host and `mkHost` can wire `meta.host` and `meta.user` into Home Manager consistently. +- Per-host user declarations should stay inline under `users.` using canonical accounts from `meta.lib.accounts`, so host-local defaults stay close to the host and `mkHost` can wire `meta.host` and `meta.user` into Home Manager consistently. - Features may rely on the `meta` contract. Existing modules already read `config.meta.host`, `config.meta.user`, and `config.meta.lib`. ## Preferred Aspect Patterns diff --git a/modules/repo-data.nix b/modules/data.nix similarity index 91% rename from modules/repo-data.nix rename to modules/data.nix index 7ae017c..50f96f1 100644 --- a/modules/repo-data.nix +++ b/modules/data.nix @@ -1,11 +1,9 @@ { lib, ... }: let - users = { + userSpecs = { kiri = { - name = "kiri"; realName = "Jelle Spreeuwenberg"; homeDirectory = "/home/kiri"; - terminalPackagePath = [ "kitty" ]; emails = { personal = { address = "mail@jelles.net"; @@ -33,10 +31,8 @@ let }; ergon = { - name = "ergon"; realName = "Jelle Spreeuwenberg"; homeDirectory = "/home/ergon"; - terminalPackagePath = [ "kitty" ]; emails = { personal = { address = "mail@jelles.net"; @@ -54,6 +50,8 @@ let }; }; + accounts = lib.mapAttrs (name: spec: spec // { inherit name; }) userSpecs; + repo = { contact.email = "mail@jelles.net"; @@ -67,6 +65,12 @@ let command = "nautilus"; packagePath = [ "nautilus" ]; }; + + terminal = { + command = "kitty"; + desktopId = "kitty.desktop"; + packagePath = [ "kitty" ]; + }; }; services = { @@ -178,13 +182,13 @@ in readOnly = true; }; - options.meta.lib.users = lib.mkOption { + options.meta.lib.accounts = lib.mkOption { type = lib.types.attrs; - description = "Canonical user attrsets shared by host definitions."; + description = "Canonical account attrsets shared by host definitions."; internal = true; readOnly = true; }; config.meta.lib.repo = repo; - config.meta.lib.users = users; + config.meta.lib.accounts = accounts; } diff --git a/modules/features/meta.nix b/modules/features/meta.nix index 3db38b5..43e4f08 100644 --- a/modules/features/meta.nix +++ b/modules/features/meta.nix @@ -27,6 +27,13 @@ let hasRequiredScopedEmail = scope: user: scopeEmailCount scope user == 1; + scopeEmailAddress = + scope: user: + let + emails = lib.filter (email: email.scope == scope) (builtins.attrValues user.emails); + in + if emails == [ ] then "" else (builtins.head emails).address; + primaryEmailFallback = { address = ""; primary = false; @@ -125,6 +132,22 @@ let type = sourceControlScopeType; default = "personal"; }; + + scopes = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { ... }: + { + options.email = lib.mkOption { + type = lib.types.str; + readOnly = true; + }; + } + ) + ); + readOnly = true; + description = "Derived source-control identities keyed by scope."; + }; }; } ); @@ -149,10 +172,6 @@ let type = lib.types.str; }; - terminalPackagePath = lib.mkOption { - type = lib.types.listOf lib.types.str; - }; - nixosConfigurationPath = lib.mkOption { type = lib.types.str; default = "${config.homeDirectory}/.config/nixos"; @@ -174,7 +193,12 @@ let }; }; - config.primaryEmail = primaryEmail; + config = { + primaryEmail = primaryEmail; + sourceControl.scopes = lib.genAttrs (requiredSourceControlScopes config) (scope: { + email = scopeEmailAddress scope config; + }); + }; } ); diff --git a/modules/features/networking.nix b/modules/features/networking.nix index c914340..a71af55 100644 --- a/modules/features/networking.nix +++ b/modules/features/networking.nix @@ -3,7 +3,6 @@ networking = { firewall.enable = true; firewall.allowPing = false; - nftables.enable = true; }; }; diff --git a/modules/features/niri/default.nix b/modules/features/niri/default.nix index f9b944f..975c97b 100644 --- a/modules/features/niri/default.nix +++ b/modules/features/niri/default.nix @@ -56,15 +56,13 @@ in } ) config.meta.host.displays; inputProfiles = metaLib.mkInputProfiles config.meta.host.input; - terminal = metaLib.resolveUserTerminal { + terminal = metaLib.resolveRepoTerminal { inherit pkgs; - user = config.meta.user; }; in { assertions = metaLib.mkTerminalAssertions { inherit terminal; - user = config.meta.user; }; home.sessionVariables.NIXOS_OZONE_WL = "1"; @@ -154,7 +152,7 @@ in }; binds = - if terminal.hasMainProgram then + if terminal.hasPackage then import ./_bindings.nix { inherit browserCommand diff --git a/modules/features/noctalia.nix b/modules/features/noctalia.nix index 1f61dab..a960723 100644 --- a/modules/features/noctalia.nix +++ b/modules/features/noctalia.nix @@ -6,6 +6,17 @@ let homeModules = config.flake.modules.homeManager; metaLib = config.meta.lib; + mkNoctaliaSettings = + { + lib, + terminalPackage, + }: + import ./_noctalia-config.nix { + inherit + lib + terminalPackage + ; + }; mkPortableSettings = baseSettings: lib.recursiveUpdate baseSettings { @@ -29,38 +40,56 @@ let }; in { - flake.modules.homeManager.noctalia = + flake.modules.homeManager.noctalia-base = { - inputs, config, lib, pkgs, ... }: let - terminal = metaLib.resolveUserTerminal { + terminal = metaLib.resolveRepoTerminal { inherit pkgs; - user = config.meta.user; }; baseSettings = - if terminal.hasMainProgram then - import ./_noctalia-config.nix { - inherit - lib - ; + if terminal.hasPackage then + mkNoctaliaSettings { + inherit lib; terminalPackage = terminal.package; } else { }; in { - imports = [ inputs.noctalia.homeModules.default ]; - - assertions = metaLib.mkTerminalAssertions { - inherit terminal; - user = config.meta.user; + options.meta.lib.noctaliaBaseSettings = lib.mkOption { + type = lib.types.attrs; + internal = true; + readOnly = true; }; + config = { + meta.lib.noctaliaBaseSettings = baseSettings; + + assertions = metaLib.mkTerminalAssertions { + inherit terminal; + }; + }; + }; + + flake.modules.homeManager.noctalia = + { + config, + inputs, + lib, + pkgs, + ... + }: + { + imports = [ + homeModules.noctalia-base + inputs.noctalia.homeModules.default + ]; + programs.noctalia-shell = { enable = true; package = lib.mkForce ( @@ -69,7 +98,7 @@ in } ); - settings = baseSettings; + settings = config.meta.lib.noctaliaBaseSettings; }; }; @@ -77,29 +106,15 @@ in { config, lib, - pkgs, ... }: - let - terminal = metaLib.resolveUserTerminal { - inherit pkgs; - user = config.meta.user; - }; - baseSettings = - if terminal.hasMainProgram then - import ./_noctalia-config.nix { - inherit - lib - ; - terminalPackage = terminal.package; - } - else - { }; - in { imports = [ homeModules.noctalia ]; programs.noctalia-shell.settings = lib.mkForce ( - if terminal.hasMainProgram then mkPortableSettings baseSettings else { } + if config.meta.lib.noctaliaBaseSettings == { } then + { } + else + mkPortableSettings config.meta.lib.noctaliaBaseSettings ); }; } diff --git a/modules/features/source-control.nix b/modules/features/source-control.nix index f35cef7..3545751 100644 --- a/modules/features/source-control.nix +++ b/modules/features/source-control.nix @@ -13,19 +13,19 @@ in host = config.meta.host; user = config.meta.user; sourceControl = user.sourceControl; + sourceControlScopes = sourceControl.scopes; hostSourceControlUsers = host.sourceControl.users; hostUserSourceControl = hostSourceControlUsers.${user.name} or { }; - scopeEmails = scope: lib.filter (email: email.scope == scope) (builtins.attrValues user.emails); + scopeConfig = scope: hostUserSourceControl.${scope} or null; + scopeIdentity = scope: sourceControlScopes.${scope} or null; emailForScope = scope: let - emails = scopeEmails scope; + identity = scopeIdentity scope; in - if builtins.length emails == 1 then (builtins.head emails).address else null; - - scopeConfig = scope: hostUserSourceControl.${scope} or null; + if identity == null then null else identity.email; scopeHasSigningKey = scope: @@ -51,10 +51,7 @@ in in if keyConfig == null then null else keyConfig.publicKey; - scopesInUse = lib.unique ([ - "personal" - sourceControl.projectScope - ]); + scopesInUse = builtins.attrNames sourceControlScopes; allowedSignersLines = map (scope: "${emailForScope scope} ${publicKeyForScope scope}") ( builtins.filter (scope: emailForScope scope != null && scopeHasSigningKey scope) scopesInUse diff --git a/modules/features/terminal.nix b/modules/features/terminal.nix index c85f26e..027b569 100644 --- a/modules/features/terminal.nix +++ b/modules/features/terminal.nix @@ -14,17 +14,14 @@ in let repoTheme = metaRepo.theme.kanagawa; palette = repoTheme.palette; - terminal = metaLib.resolveUserTerminal { + terminal = metaLib.resolveRepoTerminal { inherit pkgs; - user = config.meta.user; }; in { assertions = metaLib.mkTerminalAssertions { inherit terminal; - user = config.meta.user; requireDesktopEntry = true; - requireKitty = true; }; xdg.terminal-exec = { diff --git a/modules/features/workstation-base.nix b/modules/features/workstation-base.nix index e5bd90f..bcc43b1 100644 --- a/modules/features/workstation-base.nix +++ b/modules/features/workstation-base.nix @@ -29,6 +29,13 @@ in environment.localBinInPath = true; }; + flake.modules.nixos.workstation-host = { + imports = [ + nixosModules.workstation-base + nixosModules.qbittorrent-client + ]; + }; + flake.modules.homeManager.workstation-base = { imports = [ homeModules.ai diff --git a/modules/flake-parts.nix b/modules/flake-parts.nix index 3f43942..eb6092b 100644 --- a/modules/flake-parts.nix +++ b/modules/flake-parts.nix @@ -12,7 +12,7 @@ in { imports = [ inputs.flake-parts.flakeModules.modules - ./repo-data.nix + ./data.nix ]; systems = [ "x86_64-linux" ]; diff --git a/modules/hosts/orion/default.nix b/modules/hosts/orion/default.nix index 352d9a1..4dc647c 100644 --- a/modules/hosts/orion/default.nix +++ b/modules/hosts/orion/default.nix @@ -17,15 +17,13 @@ in ... }: let - terminal = metaLib.resolveUserTerminal { + terminal = metaLib.resolveRepoTerminal { inherit pkgs; - user = config.meta.host.users.kiri; }; in { assertions = metaLib.mkTerminalAssertions { inherit terminal; - user = config.meta.host.users.kiri; requireTerminfo = true; }; @@ -45,7 +43,7 @@ in name = "orion"; users = { kiri = { - account = metaLib.users.kiri; + account = metaLib.accounts.kiri; homeImports = [ homeModules.shell homeModules.git diff --git a/modules/hosts/polaris/default.nix b/modules/hosts/polaris/default.nix index 5875635..025fbbb 100644 --- a/modules/hosts/polaris/default.nix +++ b/modules/hosts/polaris/default.nix @@ -7,6 +7,13 @@ let nixosModules = config.flake.modules.nixos; homeModules = config.flake.modules.homeManager; metaLib = config.meta.lib; + workstationHomeImports = [ homeModules.workstation-base ]; + kiriHomeImports = workstationHomeImports ++ [ + homeModules.syncthing + homeModules.qbittorrent-client + homeModules.noctalia + ]; + ergonHomeImports = workstationHomeImports ++ [ homeModules.noctalia ]; in { flake.modules.nixos.polaris = metaLib.mkHost { @@ -31,29 +38,20 @@ in users = { kiri = { - account = metaLib.users.kiri; + account = metaLib.accounts.kiri; needsPassword = true; - homeImports = [ - homeModules.workstation-base - homeModules.syncthing - homeModules.qbittorrent-client - homeModules.noctalia - ]; + homeImports = kiriHomeImports; }; ergon = { - account = metaLib.users.ergon; + account = metaLib.accounts.ergon; needsPassword = true; - homeImports = [ - homeModules.workstation-base - homeModules.noctalia - ]; + homeImports = ergonHomeImports; }; }; imports = [ - nixosModules.workstation-base - nixosModules.qbittorrent-client + nixosModules.workstation-host nixosModules.steam ./_hardware.nix ] diff --git a/modules/hosts/zenith/default.nix b/modules/hosts/zenith/default.nix index 0d019b3..0507ef9 100644 --- a/modules/hosts/zenith/default.nix +++ b/modules/hosts/zenith/default.nix @@ -7,6 +7,14 @@ let nixosModules = config.flake.modules.nixos; homeModules = config.flake.modules.homeManager; metaLib = config.meta.lib; + workstationHomeImports = [ homeModules.workstation-base ]; + portableNoctalia = homeModules.noctalia-portable; + kiriHomeImports = workstationHomeImports ++ [ + homeModules.syncthing + homeModules.qbittorrent-client + portableNoctalia + ]; + ergonHomeImports = workstationHomeImports ++ [ portableNoctalia ]; in { flake.modules.nixos.zenith = metaLib.mkHost { @@ -39,29 +47,20 @@ in users = { kiri = { - account = metaLib.users.kiri; + account = metaLib.accounts.kiri; needsPassword = true; - homeImports = [ - homeModules.workstation-base - homeModules.syncthing - homeModules.qbittorrent-client - homeModules.noctalia-portable - ]; + homeImports = kiriHomeImports; }; ergon = { - account = metaLib.users.ergon; + account = metaLib.accounts.ergon; needsPassword = true; - homeImports = [ - homeModules.workstation-base - homeModules.noctalia-portable - ]; + homeImports = ergonHomeImports; }; }; imports = [ - nixosModules.workstation-base - nixosModules.qbittorrent-client + nixosModules.workstation-host nixosModules.laptop-power { hardware.enableRedistributableFirmware = true; diff --git a/modules/lib.nix b/modules/lib.nix index b6c18d0..f69efa4 100644 --- a/modules/lib.nix +++ b/modules/lib.nix @@ -31,17 +31,9 @@ let let hostUserSpecs = lib.mapAttrs (_: spec: normalizeHostUser 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 @@ -69,7 +61,7 @@ let users.users = lib.mapAttrs ( userName: spec: { - name = spec.account.name; + name = userName; home = spec.account.homeDirectory; isNormalUser = true; shell = pkgs.zsh; @@ -131,67 +123,59 @@ let }: lib.attrByPath path null pkgs; - resolveUserTerminal = + resolveRepoTerminal = { pkgs, - user, }: let + terminal = config.meta.lib.repo.desktop.terminal; package = resolvePackagePath { inherit pkgs; - path = user.terminalPackagePath; + path = terminal.packagePath; }; - 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 ; + inherit (terminal) + command + desktopId + packagePath + ; + + hasPackage = package != null; hasDesktopEntry = - desktopId != null && builtins.pathExists "${package}/share/applications/${desktopId}"; + package != null && builtins.pathExists "${package}/share/applications/${terminal.desktopId}"; - hasTerminfo = hasPackage && lib.elem "terminfo" package.outputs; + hasTerminfo = package != null && 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}`."; + message = "Unknown terminal package `${lib.showAttrPath terminal.packagePath}` in `meta.lib.repo.desktop.terminal`."; + } + { + assertion = terminal.command == "kitty"; + message = "The repo terminal is currently expected to be kitty."; } ] - (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."; + message = "Terminal package `${lib.showAttrPath terminal.packagePath}` must provide `${terminal.desktopId}`."; }) (lib.optional requireTerminfo { assertion = terminal.hasTerminfo; - message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must provide a `terminfo` output."; + message = "Terminal package `${lib.showAttrPath terminal.packagePath}` must provide a `terminfo` output."; }) ]; @@ -356,9 +340,9 @@ in readOnly = true; }; - options.meta.lib.resolveUserTerminal = lib.mkOption { + options.meta.lib.resolveRepoTerminal = lib.mkOption { type = lib.types.raw; - description = "Internal helper to resolve and validate user terminal metadata."; + description = "Internal helper to resolve and validate the repo-standard terminal."; internal = true; readOnly = true; }; @@ -384,7 +368,7 @@ in mkTerminalAssertions mkHost resolvePackagePath - resolveUserTerminal + resolveRepoTerminal ; }; }