diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 7a4c55fa..34bd7c36 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -15,7 +15,7 @@ dot/ │ ├── nix-oldstar/ # NixOS host │ └── mac-brightstar/ # Darwin host ├── lib/ -│ ├── default.nix # Core helpers: mkModuleV2, mkSystemModuleV2, mkDotfilesSymlink, etc. +│ ├── default.nix # Core helpers: mkFeature, mkSystemModuleV2, mkDotfilesSymlink, etc. │ ├── flakeHelpers.nix # mkDarwinConfiguration, mkNixosConfiguration │ ├── moduleRegistry.nix # Module discovery/registry │ └── themes/ # Theme system (rose-pine-moon, etc.) @@ -80,13 +80,13 @@ Use `lib.recursiveUpdate` (not `//`) to merge nested attrsets without clobbering ### Module dependency requires -Modules can declare `requires = [ "home.other.module" ]` in `mkModuleV2`. Enabling a module auto-enables its full transitive dependency tree — if A requires B and B requires C, enabling A auto-enables B and C. Inspect the registry: `nix eval '.#moduleRegistry.modules' --json`. +Modules can declare `requires = [ "home.other.module" ]` in `mkFeature`. Enabling a module auto-enables its full transitive dependency tree — if A requires B and B requires C, enabling A auto-enables B and C. Inspect the registry: `nix eval '.#moduleRegistry.modules' --json`. ### Two module types | Type | Location | Created with | Enabled via | Auto-discovered | |------|----------|-------------|-------------|-----------------| -| User | `modules/home/` | `mkModuleV2` | `hostUsers..modules..enable` | Yes | +| User | `modules/home/` | `mkFeature` | `hostUsers..modules..enable` | Yes | | System | `modules/systems/` | `mkSystemModuleV2` | `modules.system.all.*` / `modules.system.linux.*` / `modules.system.darwin.*` | Yes | ### Hierarchical enables — home modules @@ -112,34 +112,42 @@ Individual module enables still work as before (and take precedence for opt-outs Path follows directory structure: `modules/systems//` → `modules.system..` -### Platform sections in modules +### Feature structure (mkFeature) ```nix -module = { - allSystems.home.packages = [ ... ]; # Both platforms - nixosSystems.home.packages = [ ... ]; # Linux only - darwinSystems.homebrew.casks = [ ... ]; # macOS only -}; +lib.my.mkFeature { + description = "Feature description"; + platforms = [ "linux" "darwin" ]; # Supported platforms + requires = [ "home.shell.zsh" ]; # Dependencies auto-enabled per user + extraOptions = { ... }; # Per-user typed extra options + + dotfiles = { # Optional dotfiles symlinking + path = "app"; + source = "modules/home/.../dotfiles"; + }; -systemModule = { - nixosSystems = { programs.hyprland.enable = true; }; - darwinSystems = { ... }; - allSystems = { ... }; + homeModule = { config, osConfig, pkgs, lib, featureCfg, userName, ... }: { + # Home Manager configuration scoped exclusively to this user + }; + homeModuleLinux = { ... }: { ... }; # Linux-only HM config + homeModuleDarwin = { ... }: { ... }; # Darwin-only HM config + nixosModule = { config, pkgs, lib, featureCfg, selectedUsers, ... }: { ... }; # System-level NixOS activation + darwinModule = { config, pkgs, lib, featureCfg, selectedUsers, ... }: { ... }; # System-level Darwin activation }; ``` -Home-manager attrs (`home`, `programs`, `xdg`, `services`, etc.) are automatically routed to all enabled `hostUsers`. +Home-manager configuration is scoped strictly per-user to users who enable the feature. ## Key Helpers (lib/default.nix) -- `lib.my.mkModuleV2 args { module, systemModule?, dotfiles?, imports?, extraOptions?, ... }` — User modules +- `lib.my.mkFeature { description?, platforms?, requires?, extraOptions?, dotfiles?, homeModule?, homeModuleLinux?, homeModuleDarwin?, nixosModule?, darwinModule? }` — User feature descriptors - `lib.my.mkSystemModuleV2 args { namespace, module, moduleLinux?, moduleDarwin?, ... }` — System modules - `lib.my.shouldEnableModule { config, modulePath }` — Check hierarchical enable +- `lib.my.isFeatureEnabledForUser { config, userName, modulePath }` — Check per-user feature enable - `lib.my.getUsersWithModule { config, modulePath }` — Users with module enabled - `lib.my.getUsersWithModuleNames { config, _modulePath }` — Usernames list - `lib.my.mkDotfilesSymlink { config, self, path, source, target? }` — Live-editable dotfile symlinks - `lib.my.mkUsersAttrs usernames fn` — Creates `users.users` attrset -- `lib.my.mkModule system config cfg` — Low-level platform-aware config builder - `lib.my.getTheme config` — Get active theme (set via `config.theme.name`) ## Live-Editable Dotfiles diff --git a/.agents/skills/nix-module-creation/SKILL.md b/.agents/skills/nix-module-creation/SKILL.md index 2f74acf7..777c1fdf 100644 --- a/.agents/skills/nix-module-creation/SKILL.md +++ b/.agents/skills/nix-module-creation/SKILL.md @@ -1,28 +1,28 @@ --- name: nix-module-creation -description: Creating new NixOS user and system modules using mkModuleV2/mkSystemModuleV2 +description: Creating new NixOS user features and system modules using mkFeature/mkSystemModuleV2 --- -# Creating NixOS Modules +# Creating NixOS Modules and Features -## User Module (mkModuleV2) +## User Feature (mkFeature) -### Simple module +### Simple feature ```nix # modules/home//.nix -{ lib, pkgs, ... }@args: +{ lib, pkgs, ... }: -lib.my.mkModuleV2 args { - description = "Description of the module"; +lib.my.mkFeature { + description = "Description of the feature"; platforms = [ "linux" "darwin" ]; # optional, defaults to both - module = { - allSystems.home.packages = [ pkgs.my-tool ]; - # Or platform-specific: - # nixosSystems.home.packages = [ pkgs.linux-tool ]; - # darwinSystems.homebrew.casks = [ "mac-tool" ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.my-tool ]; }; + # Or platform-specific: + # homeModuleLinux = { pkgs, ... }: { home.packages = [ pkgs.linux-tool ]; }; + # darwinModule = { homebrew.casks = [ "mac-tool" ]; }; } ``` @@ -30,13 +30,13 @@ lib.my.mkModuleV2 args { ```nix # modules/home///.nix -{ lib, pkgs, ... }@args: +{ lib, pkgs, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Tool with live-editable config"; - module = { - allSystems.home.packages = [ pkgs.my-tool ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.my-tool ]; }; dotfiles = { @@ -57,20 +57,18 @@ modules/home/// ### With system-level config ```nix -{ lib, pkgs, config, ... }@args: +{ lib, pkgs, ... }: -lib.my.mkModuleV2 args { - description = "Module with system-level configuration"; +lib.my.mkFeature { + description = "Feature with system-level configuration"; - systemModule = { - nixosSystems = { - programs.hyprland.enable = true; - services.greetd.enable = true; - }; + nixosModule = { ... }: { + programs.hyprland.enable = true; + services.greetd.enable = true; }; - module = { - nixosSystems.home.packages = [ pkgs.hyprland ]; + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.hyprland ]; }; dotfiles = { @@ -83,10 +81,10 @@ lib.my.mkModuleV2 args { ### With custom options ```nix -{ lib, pkgs, ... }@args: +{ lib, pkgs, ... }: -lib.my.mkModuleV2 args { - description = "Module with custom options"; +lib.my.mkFeature { + description = "Feature with custom options"; extraOptions = { showHostname = lib.mkOption { @@ -96,49 +94,46 @@ lib.my.mkModuleV2 args { }; }; - # When using extraOptions, module becomes a function receiving cfg - module = cfg: { - allSystems.home.packages = [ pkgs.my-tool ]; - # Access option: cfg.showHostname + homeModule = { pkgs, featureCfg, ... }: { + home.packages = [ pkgs.my-tool ]; + # Access option: featureCfg.showHostname }; } ``` -### With imports (sub-modules) +### With dependencies (requires) ```nix -{ lib, pkgs, ... }@args: +{ lib, pkgs, ... }: -lib.my.mkModuleV2 args { - description = "Module with sub-module imports"; +lib.my.mkFeature { + description = "Feature with dependencies"; - imports = [ - ./dotfiles/general.nix - ./dotfiles/plugins/airline.nix + requires = [ + "home.shell.zsh" + "home.desktop.services.audio" ]; - systemModule = { - allSystems.programs.nixvim.enable = true; - }; - - module = { - allSystems.home.packages = [ pkgs.ripgrep pkgs.fzf ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.my-tool ]; }; } ``` -## mkModuleV2 Parameters +## mkFeature Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `description` | string | No | Module description | +| `description` | string | No | Feature description | | `platforms` | list | No | `["linux" "darwin"]` (default: both) | -| `requires` | list | No | Module path dependencies | -| `module` | attrset or `cfg -> attrset` | No | Platform config with `allSystems`/`nixosSystems`/`darwinSystems` sections | -| `systemModule` | attrset or `cfg -> attrset` | No | System-level config (same sections) | -| `dotfiles` | `{ path, source, target? }` | No | Live-editable symlinks | -| `extraOptions` | attrset | No | Additional NixOS options (merged with `enable`) | -| `imports` | list | No | Sub-module imports | +| `requires` | list | No | Feature path dependencies (auto-enabled per user) | +| `extraOptions` | attrset | No | Additional typed NixOS/HM options (merged per user) | +| `dotfiles` | `{ path, source, target?, live? }` | No | Live-editable symlinks | +| `homeModule` | function or attrset | No | Home Manager config for users enabling the feature | +| `homeModuleLinux` | function or attrset | No | Linux-only Home Manager config | +| `homeModuleDarwin` | function or attrset | No | Darwin-only Home Manager config | +| `nixosModule` | function or attrset | No | System-level NixOS config when any user enables the feature | +| `darwinModule` | function or attrset | No | System-level Darwin config when any user enables the feature | ## System Module (mkSystemModuleV2) @@ -167,14 +162,14 @@ lib.my.mkSystemModuleV2 args { } ``` -Enable: `systemLinux...enable = true` in host config. +Enable: `modules.system.linux...enable = true` in host config. ## Checklist 1. Decide scope: System (`systems/*`) or User (`home/*`) 2. Choose/create category subdirectory 3. Create module file using templates above -4. **System modules only**: Add import to `systems/*/default.nix` -5. **User modules**: Auto-discovered — just add enable path to `hostUsers..modules` +4. **System modules only**: Auto-discovered or added under `modules/systems/{all,linux,darwin}` +5. **User features**: Auto-discovered — just add enable path to `hostUsers..modules` 6. `git add` new files before `nix flake check` 7. Test: `nix flake check` diff --git a/checks.nix b/checks.nix index f9a41137..acb0cf46 100644 --- a/checks.nix +++ b/checks.nix @@ -2,36 +2,117 @@ let user = "dgarifullin"; - hasPackage = prefix: packages: - lib.any (package: lib.hasPrefix prefix (package.name or "")) packages; + hasPackage = nameOrPrefix: packages: + lib.any + (package: + let + pname = package.pname or ""; + name = package.name or ""; + in + pname == nameOrPrefix || + lib.hasPrefix "${nameOrPrefix}-" name || + lib.hasPrefix nameOrPrefix name || + name == nameOrPrefix + ) + packages; neovimConfig = inputs.nvim-nix.checks.${system}.neovim-config; + + commonChecks = { + mcp-config-generation = + let + goldstarHome = self.nixosConfigurations.nix-goldstar.config.home-manager.users.${user}.home; + mcpFile = goldstarHome.file.".gemini/antigravity/mcp_config.json" or null; + mcpJson = if mcpFile != null then builtins.fromJSON mcpFile.text else { }; + in + assert goldstarHome.file ? ".gemini/antigravity/mcp_config.json"; + assert mcpJson ? mcpServers.nixos; + assert mcpJson ? mcpServers.ghidra; + pkgs.runCommand "mcp-config-generation-check" { } "touch $out"; + + multi-user-isolation = + let + testHost = self.nixosConfigurations.nix-goldstar.extendModules { + modules = [ + { + hostUsers.bob = { + enable = true; + modules = { + home.cli.htop.enable = true; + home.games.soundpad.enable = true; + }; + }; + } + ]; + }; + bobPkgs = testHost.config.home-manager.users.bob.home.packages; + bobFiles = testHost.config.home-manager.users.bob.home.file; + bobGroups = testHost.config.users.users.bob.extraGroups or [ ]; + dgarifullinPkgs = testHost.config.home-manager.users.${user}.home.packages; + dgarifullinFiles = testHost.config.home-manager.users.${user}.home.file; + dgarifullinGroups = testHost.config.users.users.${user}.extraGroups or [ ]; + in + # bob receives htop + assert hasPackage "htop" bobPkgs; + # bob receives pamixer/pavucontrol (via transitive requires audio) + assert hasPackage "pamixer" bobPkgs; + assert hasPackage "pavucontrol" bobPkgs; + # bob does NOT receive telegram-desktop, google-antigravity-ide, .gemini/antigravity/mcp_config.json, or docker group + assert !(hasPackage "telegram-desktop" bobPkgs); + assert !(hasPackage "google-antigravity" bobPkgs); + assert !(bobFiles ? ".gemini/antigravity/mcp_config.json"); + assert !(lib.elem "docker" bobGroups); + # dgarifullin retains all of their packages, configs, and groups + assert hasPackage "htop" dgarifullinPkgs; + assert hasPackage "telegram-desktop" dgarifullinPkgs; + assert (dgarifullinFiles ? ".gemini/antigravity/mcp_config.json"); + assert (lib.elem "docker" dgarifullinGroups); + pkgs.runCommand "multi-user-isolation-check" { } "touch $out"; + + cascade-child-opt-out = + let + testDarwin = self.darwinConfigurations.mac-brightstar.extendModules { + modules = [ + { + hostUsers.${user}.modules = { + home.utils.enable = lib.mkForce true; + home.utils.raycast.enable = lib.mkForce false; + }; + } + ]; + }; + pkgsList = testDarwin.config.home-manager.users.${user}.home.packages; + in + # obsidian is enabled via category cascade + assert hasPackage "obsidian" pkgsList; + # raycast remains disabled via child opt-out + assert !(hasPackage "raycast" pkgsList); + pkgs.runCommand "cascade-child-opt-out-check" { } "touch $out"; + + neovim-config = neovimConfig; + }; in if system == "x86_64-linux" then let home = self.nixosConfigurations.nix-goldstar.config.home-manager.users.${user}.home; in - { + commonChecks // { module-platform-merge = assert hasPackage "ripgrep-" home.packages; assert hasPackage "wl-clipboard-" home.packages; assert hasPackage "google-antigravity-ide-with-basic-store" home.packages; assert home.file ? ".gemini/config/AGENTS.md"; pkgs.runCommand "module-platform-merge-check" { } "touch $out"; - - neovim-config = neovimConfig; } else if system == "aarch64-darwin" then let home = self.darwinConfigurations.mac-brightstar.config.home-manager.users.${user}.home; in - { + commonChecks // { module-platform-merge = assert hasPackage "ripgrep-" home.packages; assert !(hasPackage "wl-clipboard-" home.packages); pkgs.runCommand "module-platform-merge-check" { } "touch $out"; - - neovim-config = neovimConfig; } else { } diff --git a/extensions/empty/flake.nix b/extensions/empty/flake.nix new file mode 100644 index 00000000..5ed567d8 --- /dev/null +++ b/extensions/empty/flake.nix @@ -0,0 +1,15 @@ +{ + description = "Default empty private extension stub for dot"; + + outputs = { self }: { + dotExtension = { + features = [ ]; + systemModules = { + all = [ ]; + linux = [ ]; + darwin = [ ]; + }; + hosts = { }; + }; + }; +} diff --git a/flake.lock b/flake.lock index dd08f04f..04da7748 100644 --- a/flake.lock +++ b/flake.lock @@ -316,6 +316,18 @@ "type": "github" } }, + "private-extension": { + "locked": { + "lastModified": 1, + "narHash": "sha256-qUFY+RNeKb60AJ0AKlZanfta+KYQLs4L5DsRVTPNkpQ=", + "path": "./extensions/empty", + "type": "path" + }, + "original": { + "path": "./extensions/empty", + "type": "path" + } + }, "root": { "inputs": { "antigravity-nix": "antigravity-nix", @@ -328,6 +340,7 @@ "nix-wrapper-modules": "nix-wrapper-modules", "nixpkgs": "nixpkgs_2", "nvim-nix": "nvim-nix", + "private-extension": "private-extension", "sops-nix": "sops-nix", "vscode-server": "vscode-server" } diff --git a/flake.nix b/flake.nix index b011a0f5..07a8ae7b 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,10 @@ url = "github:gdr/clavis"; inputs.nixpkgs.follows = "nixpkgs"; }; + + private-extension = { + url = "path:./extensions/empty"; + }; }; outputs = inputs@{ self, nixpkgs, charon-key, ... }: diff --git a/hosts/machines/mac-brightstar/default.nix b/hosts/machines/mac-brightstar/default.nix index 27bb08a5..818b69ca 100644 --- a/hosts/machines/mac-brightstar/default.nix +++ b/hosts/machines/mac-brightstar/default.nix @@ -75,7 +75,6 @@ in modules.system.all = { nix.settings.enable = true; nix.gc.enable = true; - shell.git.enable = true; shell.ssh.enable = true; sops.enable = true; # fonts.enable = true; # disabled on Darwin @@ -93,7 +92,6 @@ in }; }; - modules.home.editors.antigravity = { enable = true; } // userDefaults.antigravity; time.timeZone = "Europe/Moscow"; theme.name = "rose-pine-moon"; diff --git a/hosts/machines/nix-goldstar/default.nix b/hosts/machines/nix-goldstar/default.nix index b9b45073..f64db779 100644 --- a/hosts/machines/nix-goldstar/default.nix +++ b/hosts/machines/nix-goldstar/default.nix @@ -51,7 +51,6 @@ in # host-specific home.browsers.vivaldi.enable = true; home.desktop = { - gnome.enable = true; hyprland.enable = true; }; home.editors.neovim.enable = true; @@ -88,10 +87,6 @@ in nix.settings.enable = true; nix.gc.enable = true; fonts.enable = true; - }; - - modules.system.all = { - shell.git.enable = true; shell.ssh.enable = true; }; diff --git a/hosts/machines/nix-oldstar/default.nix b/hosts/machines/nix-oldstar/default.nix index d11fb142..266f4f05 100644 --- a/hosts/machines/nix-oldstar/default.nix +++ b/hosts/machines/nix-oldstar/default.nix @@ -58,7 +58,6 @@ in nix.settings.enable = true; nix.gc.enable = true; fonts.enable = true; - shell.git.enable = true; shell.ssh.enable = true; sops.enable = true; }; diff --git a/lib/default.nix b/lib/default.nix index 58cbb3bc..5a056eda 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -2,6 +2,20 @@ # Import module registry functions moduleRegistry = import ./moduleRegistry.nix { inherit lib; }; + # Native feature compiler and mkFeature descriptor + features = import ./features.nix { inherit lib inputs; }; + inherit (features) + computeModulePath + mkFeature + buildFeatureOptionTree + isFeatureEnabledForUser + resolveUserFeatures + compileUserDotfiles + compileUserHomeModules + compileSystemFeatureModules; + + mkUserDotfilesConfig = compileUserDotfiles; + # ── Theme system ───────────────────────────────────────────────────────── # Theme registry: lib.my.themes.getTheme "rose-pine-moon" themes = import ./themes { inherit lib; }; @@ -68,47 +82,89 @@ # This scans modules and extracts metadata, returning a registry structure buildModuleRegistry = modulesDir: prefix: let - moduleTree = recursiveDirs modulesDir; - moduleFiles = flattenModules moduleTree; - - # Filter out tags.nix and default.nix - moduleFilesFiltered = filter - (path: - ! hasSuffix "tags.nix" path && - ! hasSuffix "default.nix" path - ) - moduleFiles; - - tryImportMeta = path: + makeRegistry = extFeaturesArg: let + moduleTree = recursiveDirs modulesDir; + moduleFiles = flattenModules moduleTree; + + # Filter out tags.nix and default.nix + moduleFilesFiltered = filter + (path: + ! hasSuffix "tags.nix" path && + ! hasSuffix "default.nix" path + ) + moduleFiles; + libWithMy = lib // { inherit (lib) my; }; - moduleResult = builtins.tryEval (import path (dummySpecialArgs // { lib = libWithMy; })); - in - if moduleResult.success then - (moduleResult.value._meta or moduleResult.value.meta or null) - else - builtins.trace "Warning: Failed to extract metadata from ${toString path}: ${toString moduleResult.value}" null; - modulesWithMeta = map - (path: - let - relativePath = removePrefix "./" (removeSuffix ".nix" path); - fullPath = prefix + "." + (replaceStrings [ "/" ] [ "." ] relativePath); - meta = tryImportMeta path; - in - { - file = path; - path = fullPath; - meta = meta; - } - ) - moduleFilesFiltered; - - modulesWithMetaList = filter (m: m.meta != null) modulesWithMeta; + tryImportFile = path: + let + fullPath = computeModulePath path; + imported = import path; + evalResult = builtins.tryEval ( + if isFunction imported + then imported (dummySpecialArgs // { lib = libWithMy; _modulePath = fullPath; }) + else imported + ); + in + if evalResult.success then + evalResult.value + else + builtins.trace "Warning: Failed to import ${toString path}: ${toString evalResult.value}" null; + + modulesWithMeta = map + (path: + let + fullPath = computeModulePath path; + modVal = tryImportFile path; + isFeature = (modVal._type or "") == "feature"; + meta = + if isFeature then + (modVal.meta or { inherit (modVal) requires platforms description; }) + else + (modVal._meta or modVal.meta or null); + in + { + file = path; + path = fullPath; + meta = meta; + isFeature = isFeature; + feature = if isFeature then (modVal // { path = fullPath; file = path; }) else null; + } + ) + moduleFilesFiltered; + + modulesWithMetaList = filter (m: m.meta != null) modulesWithMeta; + discoveredFeatures = filter (f: f != null) (map (m: m.feature) modulesWithMeta); + + extFeatures = if isList extFeaturesArg then extFeaturesArg else [ ]; + extFeaturesWithPaths = map + (f: + let + path = f.path or (if f ? name then (if hasPrefix "home." f.name then f.name else "home." + f.name) else throw "Feature in private-extension must have path or name"); + in + f // { inherit path; } + ) + extFeatures; + + repoFeaturePaths = map (f: f.path) discoveredFeatures; + extFeaturePaths = map (f: f.path) extFeaturesWithPaths; + collisions = intersectLists repoFeaturePaths extFeaturePaths; + _assert = assert assertMsg (collisions == [ ]) + "Colliding feature paths between repository and private-extension: ${toString collisions}"; true; + + allFeatures = assert _assert; discoveredFeatures ++ extFeaturesWithPaths; + in + { + modules = modulesWithMetaList; + allModules = modulesWithMeta; + features = allFeatures; + featureCatalog = allFeatures; + }; + defaultRegistry = makeRegistry [ ]; in - { - modules = modulesWithMetaList; - allModules = modulesWithMeta; + defaultRegistry // { + __functor = _: makeRegistry; }; mkBoolOpt = default: mkOption { @@ -140,103 +196,7 @@ filterPrefix = prefix: files: builtins.filter (file: builtins.hasPrefix prefix (builtins.basename file)) files; - # mkModule: Creates platform-aware config that routes user config to home-manager.users - # Usage in modules: - # let mkModule = lib.my.mkModule system config; in - # mkIf shouldEnable (mkModule { - # nixosSystems.home.packages = [ pkgs.htop ]; # → home-manager.users.*.home.packages - # nixosSystems.programs.chromium.enable = true; # → home-manager.users.*.programs.chromium - # nixosSystems.services.foo.enable = true; # System-level config (stays as-is) - # darwinSystems.homebrew.casks = [ "htop" ]; - # }) # Helper to create users.users attribute set from list of usernames - # Usage: - # users.users = lib.my.mkUsersAttrs enabledUsernames (username: { shell = pkgs.zsh; }); - # users.users = lib.my.mkUsersAttrs enabledUsernames (username: { extraGroups = [ "docker" ]; }); - # - # Or with module path (automatically gets users who enabled the module): - # users.users = lib.my.mkUsersAttrs { inherit config _modulePath; } (username: { extraGroups = [ "docker" ]; }); - mkUsersAttrs = usernamesOrModule: fn: - let - # Check if first arg is a module config (has config and _modulePath keys) or a list of usernames - isModuleConfig = builtins.isAttrs usernamesOrModule && - (usernamesOrModule ? config || usernamesOrModule ? _modulePath || usernamesOrModule ? modulePath); - usernames = - if isModuleConfig then - getUsersWithModuleNames usernamesOrModule - else - usernamesOrModule; - in - listToAttrs (map - (username: { - name = username; - value = fn username; - }) - usernames); - - # Home-manager attrs (home, programs, xdg, services, etc.) are routed to all enabled users - mkModule = system: config: cfg: - let - isDarwin = system == "aarch64-darwin" || system == "x86_64-darwin"; - isLinux = system == "aarch64-linux" || system == "x86_64-linux"; - - rawConfig = - if isLinux then (cfg.nixosSystems or { }) // (cfg.allSystems or { }) - else if isDarwin then (cfg.darwinSystems or { }) // (cfg.allSystems or { }) - else { }; - - # Home-manager user-level attributes (routed to home-manager.users.*) - # Note: systemd, gtk, qt, dconf, wayland, xresources, xsession are Linux-only - hmUserAttrsBase = [ "home" "programs" "xdg" "services" "accounts" "fonts" "manual" "news" "nix" "targets" ]; - hmUserAttrsLinux = [ "systemd" "gtk" "qt" "dconf" "wayland" "xresources" "xsession" ]; - hmUserAttrs = hmUserAttrsBase ++ (if isLinux then hmUserAttrsLinux else [ ]); - - # Extract home-manager attrs from config - userConfig = filterAttrs (name: _: elem name hmUserAttrs) rawConfig; - hasUserConfig = userConfig != { }; - - # Remove home-manager attrs from system config - systemConfig = filterAttrs (name: _: !(elem name hmUserAttrs)) rawConfig; - - # Get enabled users for home config routing - enabledUsers = filterAttrs (_: u: u.enable) (config.hostUsers or { }); - in - systemConfig // (optionalAttrs hasUserConfig { - home-manager.users = mapAttrs (name: _: userConfig) enabledUsers; - }); - - # mkModuleConfig: Combines mkModule + optional dotfiles symlink - # Usage: - # config = mkIf shouldEnable (lib.my.mkModuleConfig { - # inherit system config self; - # module = { - # nixosSystems.home.packages = [ pkgs.ghostty ]; - # darwinSystems.homebrew.casks = [ "ghostty" ]; - # }; - # dotfiles = { path = "ghostty"; source = "modules/.../dotfiles"; }; - # }); - mkModuleConfig = { system, config, self ? null, module, dotfiles ? null }: - mkMerge [ - (mkModule system config module) - (optionalAttrs (dotfiles != null && self != null) { - home-manager.users = mkDotfilesSymlink { - inherit config self; - inherit (dotfiles) path source; - }; - }) - ]; - - # Module metadata structure (for registry/discovery) - mkModuleMeta = - { requires ? [ ] - , # List of module paths: ["home.shell.git", "home.utils.bazel"] - platforms ? [ "linux" "darwin" ] - , scope ? "user" - , # "system" = system services/drivers, "user" = home.packages - description ? null - }: { - inherit requires platforms scope description; - }; # Helper to derive config accessor from module path # Usage: @@ -284,105 +244,43 @@ in anyGlobal || anyUser; + shouldEnableModuleForUser = { config, userName, modulePath ? null, featurePath ? null, modulesRegistry ? null }: + isFeatureEnabledForUser { + inherit config userName modulesRegistry; + featurePath = if featurePath != null then featurePath else modulePath; + }; + shouldEnableModule = { config, modulePath, moduleTags ? [ ], modulesRegistry ? null }: let + usersWithMod = getUsersWithModule { inherit config modulePath modulesRegistry; }; pathParts = splitString "." modulePath; - - # Generate all parent paths: ["home", "home.browsers", "home.browsers.vivaldi"] - # We check .enable on each of these - generatePaths = parts: - let - indices = genList (i: i + 1) (length parts); - in - map (i: take i parts) indices; - - allPaths = generatePaths pathParts; - - # Check if any path (including parents) has .enable = true in config.modules - checkPathEnable = parts: - let - cfg = foldl' (acc: part: acc.${part} or { }) config.modules parts; - in - cfg.enable or false; - - anyGlobalPathEnabled = any checkPathEnable allPaths; - - # Per-user check: same logic but in hostUsers..modules - enabledUsers = filterAttrs (name: ucfg: ucfg.enable or false) (config.hostUsers or { }); - - checkUserPathEnable = userModules: parts: - let - cfg = foldl' (acc: part: acc.${part} or { }) userModules parts; - in - cfg.enable or false; - - anyUserHasPathEnabled = any - (ucfg: - let - userModules = ucfg.modules or { }; - in - any (path: checkUserPathEnable userModules path) allPaths - ) - (attrValues enabledUsers); - - # Requires check: enabled if any explicitly-enabled module's transitive - # dependency tree includes this path. Uses resolveDeps for full transitivity - # (A requires B, B requires C → enabling A auto-enables both B and C). - # Uses isExplicitlyEnabled (not shouldEnableModule) to avoid circular evaluation. - isRequiredByEnabled = - if modulesRegistry == null then false - else - let - regModules = modulesRegistry.modules or [ ]; - moduleMap = listToAttrs (map (m: { name = m.path; value = m.meta; }) regModules); - in - any - (m: - m.path != modulePath && - isExplicitlyEnabled { inherit config; modulePath = m.path; } && - elem modulePath (moduleRegistry.resolveDeps moduleMap m.path [ ]) - ) - regModules; + cfg = foldl' (acc: part: acc.${part} or { }) (config.modules or { }) pathParts; + isGloballyEnabled = (cfg.enable or false) == true; in - anyGlobalPathEnabled || anyUserHasPathEnabled || isRequiredByEnabled; + isGloballyEnabled || (usersWithMod != { }); # Get list of users who have a module enabled (via any path level) # Usage: usersWithModule = lib.my.getUsersWithModule { inherit config modulePath; }; - getUsersWithModule = { config, modulePath, moduleTags ? [ ] }: + getUsersWithModule = { config, modulePath, moduleTags ? [ ], modulesRegistry ? null }: let - pathParts = splitString "." modulePath; - generatePaths = parts: - let - indices = genList (i: i + 1) (length parts); - in - map (i: take i parts) indices; - allPaths = generatePaths pathParts; - enabledUsers = filterAttrs (name: ucfg: ucfg.enable or false) (config.hostUsers or { }); - - checkUserPathEnable = userModules: parts: - let - cfg = foldl' (acc: part: acc.${part} or { }) userModules parts; - in - cfg.enable or false; in filterAttrs - (name: ucfg: - let - userModules = ucfg.modules or { }; - in - any (path: checkUserPathEnable userModules path) allPaths + (userName: _: + isFeatureEnabledForUser { + inherit config userName modulePath modulesRegistry; + } ) enabledUsers; # Get list of usernames who have a module enabled (convenience wrapper) # Usage: usernames = lib.my.getUsersWithModuleNames { inherit config _modulePath; }; # Returns: [ "user1" "user2" ] - list of usernames - getUsersWithModuleNames = { config, modulePath ? null, _modulePath ? null, moduleTags ? [ ] }: + getUsersWithModuleNames = { config, modulePath ? null, _modulePath ? null, moduleTags ? [ ], modulesRegistry ? null }: let # Support both modulePath and _modulePath (from args) path = if _modulePath != null then _modulePath else modulePath; - usersWithModule = getUsersWithModule { inherit config moduleTags; modulePath = path; }; + usersWithModule = getUsersWithModule { inherit config moduleTags modulesRegistry; modulePath = path; }; in attrNames usersWithModule; @@ -467,167 +365,7 @@ ) enabledUsers; - # Generate module options from path string - # Usage: - # options = lib.my.mkModuleOptions "home.core.htop" { - # enable = mkOption { default = false; type = types.bool; }; - # }; - # Returns: { modules.home.core.htop = { enable = ...; }; } - mkModuleOptions = modulePath: opts: - let - pathParts = splitString "." modulePath; - in - { modules = setAttrByPath pathParts opts; }; - - # Complete module wrapper for modules - # Returns { meta, options, config, imports? } - the entire module structure - # - # Modules are enabled hierarchically: - # - modules.home.browsers.vivaldi.enable = true (specific module) - # - modules.home.browsers.enable = true (all browsers) - # - modules.home.enable = true (all home modules) - # - hostUsers..modules.home.browsers.enable = true (per-user) - # - # Usage: - # { lib, pkgs, ... }@args: - # lib.my.mkModuleV2 args { - # description = "Ghostty terminal emulator"; - # module = { - # nixosSystems.home.packages = [ pkgs.ghostty ]; - # darwinSystems.homebrew.casks = [ "ghostty" ]; - # }; - # dotfiles = { - # path = "ghostty"; - # source = "modules/home/terminal/ghostty/dotfiles"; - # }; - # extraOptions = { - # showHostname = mkOption { default = true; type = types.bool; }; - # }; - # } - # Module sections: allSystems, nixosSystems, darwinSystems - # home.* config is automatically routed to all enabled hostUsers - mkModuleV2 = args: - { platforms ? [ "linux" "darwin" ] - , requires ? [ ] # List of module paths this module depends on - , description ? null - , module ? { } - , systemModule ? { } - , dotfiles ? null - , extraOptions ? { } - , imports ? [ ] - }: - let - inherit (args) config system _modulePath; - self = args.self or null; - modulesRegistry = args.modulesRegistry or null; - modulePath = _modulePath; - - # Get module config (for accessing options) - pathParts = splitString "." modulePath; - cfg = foldl' (acc: part: acc.${part} or { }) config.modules pathParts; - - isDarwin = system == "aarch64-darwin" || system == "x86_64-darwin"; - isLinux = system == "aarch64-linux" || system == "x86_64-linux"; - - # Get enabled users for home config routing (needed for shouldEnable check) - enabledUsers = filterAttrs (_: u: u.enable) (config.hostUsers or { }); - - # Automatically add `dotfilesLive` option if this module defines dotfiles - dotfilesExtraOptions = optionalAttrs (dotfiles != null) { - dotfilesLive = mkOption { - type = types.nullOr types.bool; - default = null; - description = "Override dotfile symlink mode for this module (true = live edit out-of-store, false = immutable nix store)."; - }; - }; - in - { - inherit imports; - - meta = mkModuleMeta { - inherit requires platforms description; - }; - - options = mkModuleOptions modulePath ({ - enable = mkOption { - default = false; - type = bool; - description = description; - }; - } // dotfilesExtraOptions // extraOptions); - - config = - let - shouldEnable = shouldEnableModule { inherit config modulePath modulesRegistry; }; - in - mkIf shouldEnable ( - let - # module can be attrset or function (cfg -> attrset) - resolvedModule = if isFunction module then module cfg else module; - - # Keep common and platform-specific definitions as separate module - # fragments. A shallow attrset merge would discard an entire - # top-level subtree (for example, nixosSystems.home when - # allSystems.home is also present). - selectedConfigs = - (optional (resolvedModule ? allSystems) resolvedModule.allSystems) - ++ (optional (isLinux && resolvedModule ? nixosSystems) resolvedModule.nixosSystems) - ++ (optional (isDarwin && resolvedModule ? darwinSystems) resolvedModule.darwinSystems); - - # Home-manager user-level attributes (routed to home-manager.users.*) - # Note: systemd, gtk, qt, dconf, wayland, xresources, xsession are Linux-only - hmUserAttrsBase = [ "home" "programs" "xdg" "services" "accounts" "fonts" "manual" "news" "nix" "targets" ]; - hmUserAttrsLinux = [ "systemd" "gtk" "qt" "dconf" "wayland" "xresources" "xsession" ]; - hmUserAttrs = hmUserAttrsBase ++ (if isLinux then hmUserAttrsLinux else [ ]); - - # Extract home-manager attrs from config - userConfigs = map - (fragment: filterAttrs (name: _: elem name hmUserAttrs) fragment) - selectedConfigs; - hasUserConfig = any (fragment: fragment != { }) userConfigs; - userConfig = mkMerge userConfigs; - - # Remove home-manager attrs from system config - systemConfig = mkMerge (map - (fragment: filterAttrs (name: _: !(elem name hmUserAttrs)) fragment) - selectedConfigs); - - # systemModule can be attrset or function (cfg -> attrset) - # Supports platform sections: allSystems, nixosSystems, darwinSystems - resolvedSystemModuleRaw = if isFunction systemModule then systemModule cfg else systemModule; - resolvedSystemModule = - if isLinux || isDarwin then - mkMerge - ( - (optional (resolvedSystemModuleRaw ? allSystems) resolvedSystemModuleRaw.allSystems) - ++ (optional (isLinux && resolvedSystemModuleRaw ? nixosSystems) resolvedSystemModuleRaw.nixosSystems) - ++ (optional (isDarwin && resolvedSystemModuleRaw ? darwinSystems) resolvedSystemModuleRaw.darwinSystems) - ) - else - resolvedSystemModuleRaw; - in - mkMerge [ - resolvedSystemModule - systemConfig - (optionalAttrs hasUserConfig { - home-manager.users = mapAttrs (name: _: userConfig) enabledUsers; - }) - (optionalAttrs (dotfiles != null && self != null) { - home-manager.users = mkDotfilesSymlink { - inherit config self; - inherit (dotfiles) path source; - target = dotfiles.target or null; - live = - if (cfg.dotfilesLive or null) != null then cfg.dotfilesLive - else (dotfiles.live or false); - }; - }) - ] - ); - }; - # System module wrapper for systems/all, systems/darwin, systems/linux modules - # Unlike mkModuleV2, this: # - Uses systemAll/systemDarwin/systemLinux option namespace (not modules.*) # - Has platform restrictions built-in (no need for tags) # - Passes through special inputs (overlays, inputs, hardware, etc.) diff --git a/lib/features.nix b/lib/features.nix new file mode 100644 index 00000000..276caabd --- /dev/null +++ b/lib/features.nix @@ -0,0 +1,447 @@ +# Native Feature Descriptor and Compiler (mkFeature) +# Provides per-user Home Manager isolation, typed option trees, and two-stage system activation +{ lib, inputs ? { }, ... }: + +let + moduleRegistryHelper = import ./moduleRegistry.nix { inherit lib; }; + + # Compute module path from file path + # e.g., "/path/to/modules/home/cli/htop.nix" -> "home.cli.htop" + # e.g., "/path/to/modules/home/cli/htop/htop.nix" -> "home.cli.htop" (dedup) + computeModulePath = filePath: + let + pathStr = toString filePath; + isModulePath = lib.hasInfix "modules/" pathStr; + afterModules = lib.last (lib.splitString "modules/" pathStr); + withoutNix = lib.removeSuffix ".nix" afterModules; + parts = lib.splitString "/" withoutNix; + fileName = lib.last parts; + parentDir = if lib.length parts >= 2 then lib.elemAt parts (lib.length parts - 2) else null; + dedupedParts = + if parentDir == fileName + then lib.init parts + else parts; + modulePath = lib.concatStringsSep "." dedupedParts; + in + if isModulePath then modulePath else null; + + # mkFeature: Pure declarative feature descriptor constructor + mkFeature = + { description ? null + , platforms ? [ "linux" "darwin" ] + , requires ? [ ] + , extraOptions ? { } + , dotfiles ? null + , homeModule ? null + , homeModuleLinux ? null + , homeModuleDarwin ? null + , nixosModule ? null + , darwinModule ? null + }: + assert lib.assertMsg (lib.isList platforms) "mkFeature: platforms must be a list of strings"; + assert lib.assertMsg (lib.isList requires) "mkFeature: requires must be a list of strings"; + assert lib.assertMsg (lib.isAttrs extraOptions) "mkFeature: extraOptions must be an attribute set"; + assert lib.assertMsg (dotfiles == null || lib.isAttrs dotfiles) "mkFeature: dotfiles must be null or an attribute set"; + assert lib.assertMsg (homeModule == null || lib.isFunction homeModule || lib.isAttrs homeModule) "mkFeature: homeModule must be null, a function, or an attribute set"; + assert lib.assertMsg (homeModuleLinux == null || lib.isFunction homeModuleLinux || lib.isAttrs homeModuleLinux) "mkFeature: homeModuleLinux must be null, a function, or an attribute set"; + assert lib.assertMsg (homeModuleDarwin == null || lib.isFunction homeModuleDarwin || lib.isAttrs homeModuleDarwin) "mkFeature: homeModuleDarwin must be null, a function, or an attribute set"; + assert lib.assertMsg (nixosModule == null || lib.isFunction nixosModule || lib.isAttrs nixosModule) "mkFeature: nixosModule must be null, a function, or an attribute set"; + assert lib.assertMsg (darwinModule == null || lib.isFunction darwinModule || lib.isAttrs darwinModule) "mkFeature: darwinModule must be null, a function, or an attribute set"; + { + _type = "feature"; + inherit description platforms requires extraOptions dotfiles + homeModule homeModuleLinux homeModuleDarwin nixosModule darwinModule; + meta = { + inherit description platforms requires; + }; + }; + + # Generate strongly-typed option trees for categories and leaf features + # Both options.modules.home.* and options.hostUsers..modules.home.* use this + buildFeatureOptionTree = { featureCatalog ? [ ], knownModulePaths ? [ ] }: + let + featurePaths = map (f: lib.splitString "." f.path) featureCatalog; + allPaths = (map (p: lib.splitString "." p) knownModulePaths) ++ featurePaths; + categoryPrefixes = lib.unique (lib.concatMap + (p: + let len = lib.length p; in + lib.genList (i: lib.take (i + 1) p) (len - 1) + ) + allPaths); + + isCategory = prefix: lib.any (c: c == prefix) categoryPrefixes; + + buildSubtree = prefix: + let + prefixLen = lib.length prefix; + relevantPaths = lib.filter (p: (lib.length p > prefixLen) && (lib.take prefixLen p == prefix)) + (categoryPrefixes ++ featurePaths); + childNames = lib.unique (map (p: lib.elemAt p prefixLen) relevantPaths); + + categoryOpt = + if (prefixLen > 0 && isCategory prefix) then { + enable = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Enable ${lib.concatStringsSep "." prefix} modules"; + }; + } else { }; + + matchingFeature = lib.findFirst (f: lib.splitString "." f.path == prefix) null featureCatalog; + featureOpts = + if matchingFeature != null then { + enable = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = matchingFeature.description or "Enable ${matchingFeature.path}"; + }; + } + // (lib.optionalAttrs (matchingFeature.dotfiles != null) { + dotfilesLive = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Override dotfile symlink mode for ${matchingFeature.path} (true = live edit out-of-store, false = immutable nix store)."; + }; + }) + // (matchingFeature.extraOptions or { }) + else { }; + + children = lib.listToAttrs (map + (name: { + inherit name; + value = buildSubtree (prefix ++ [ name ]); + }) + childNames); + in + categoryOpt // featureOpts // children; + in + { + home = buildSubtree [ "home" ]; + }; + + # Check if a feature is enabled for a specific user: + # 1. Walks from leaf to root in userModules: most specific non-null setting wins. + # 2. If user setting is null, falls back to global config.modules hierarchy. + # 3. If directly enabled, returns true. + # 4. If user explicitly set enable = false, returns false (transitive requires cannot override user opt-out). + # 5. Otherwise, checks per-user transitive requires via moduleRegistry.resolveDeps. + isFeatureEnabledForUser = + { config ? null + , osConfig ? null + , userName + , featurePath ? null + , modulePath ? null + , featureCatalog ? null + , modulesRegistry ? null + , ... + }: + let + cfg = if config != null then config else osConfig; + path = if featurePath != null then featurePath else modulePath; + pathParts = lib.splitString "." path; + + catalog = + if featureCatalog != null then featureCatalog + else if modulesRegistry != null then (modulesRegistry.features or modulesRegistry.featureCatalog or [ ]) + else if cfg ? _modulesRegistry && cfg._modulesRegistry != null then (cfg._modulesRegistry.features or cfg._modulesRegistry.featureCatalog or [ ]) + else [ ]; + + prefixes = lib.genList (i: lib.take (i + 1) pathParts) (lib.length pathParts); + revPrefixes = lib.reverseList prefixes; + + findSetting = modules: + lib.foldl' + (acc: prefix: + if acc != null then acc + else + let + sub = lib.foldl' (a: p: a.${p} or { }) modules prefix; + in + sub.enable or null + ) + null + revPrefixes; + + userModules = cfg.hostUsers.${userName}.modules or { }; + globalModules = cfg.modules or { }; + + userSetting = findSetting userModules; + globalSetting = findSetting globalModules; + + directlyEnabled = + if userSetting != null then userSetting + else if globalSetting != null then globalSetting + else false; + in + if directlyEnabled then true + else if userSetting == false then false # User explicit opt-out wins over transitive requires + else + # Check transitive requirements + let + registryModules = + if modulesRegistry != null then (modulesRegistry.modules or [ ]) + else if cfg ? _modulesRegistry && cfg._modulesRegistry != null then (cfg._modulesRegistry.modules or [ ]) + else [ ]; + + moduleMap = + (lib.listToAttrs (map (m: { name = m.path; value = m.meta or m; }) registryModules)) + // (lib.listToAttrs (map (f: { name = f.path; value = f; }) catalog)); + + allKnownPaths = lib.attrNames moduleMap; + + isPathDirectlyEnabled = p: + let + pParts = lib.splitString "." p; + pPrefixes = lib.genList (i: lib.take (i + 1) pParts) (lib.length pParts); + pRevPrefixes = lib.reverseList pPrefixes; + pFindSetting = modules: + lib.foldl' + (acc: prefix: + if acc != null then acc + else + let sub = lib.foldl' (a: part: a.${part} or { }) modules prefix; in + sub.enable or null + ) + null + pRevPrefixes; + uSet = pFindSetting userModules; + gSet = pFindSetting globalModules; + in + if uSet != null then uSet + else if gSet != null then gSet + else false; + + directlyEnabledPaths = lib.filter (p: p != path && isPathDirectlyEnabled p) allKnownPaths; + + isRequired = lib.any + (parentPath: + let + deps = moduleRegistryHelper.resolveDeps moduleMap parentPath [ ]; + in + lib.elem path deps + ) + directlyEnabledPaths; + in + isRequired; + + # Resolve all active features for a user + resolveUserFeatures = + { osConfig + , pkgs ? null + , userName + , userCfg + , featureCatalog ? [ ] + , modulesRegistry ? null + , hostPlatform ? ( + if pkgs != null then + (if pkgs.stdenv.hostPlatform.isDarwin then "darwin" else "linux") + else if lib.hasSuffix "-darwin" (osConfig.nixpkgs.system or "") || (osConfig ? system && osConfig.system ? primaryUser) then "darwin" + else "linux" + ) + }: + let + catalog = + if featureCatalog != [ ] then featureCatalog + else if modulesRegistry != null then (modulesRegistry.features or modulesRegistry.featureCatalog or [ ]) + else if osConfig ? _modulesRegistry && osConfig._modulesRegistry != null then (osConfig._modulesRegistry.features or osConfig._modulesRegistry.featureCatalog or [ ]) + else [ ]; + in + lib.filter + (f: + (lib.elem hostPlatform f.platforms) + && isFeatureEnabledForUser { + config = osConfig; + inherit userName modulesRegistry; + featurePath = f.path; + featureCatalog = catalog; + } + ) + catalog; + + # Single-user dotfiles compilation (scoped strictly to the user who enabled the feature) + compileUserDotfiles = + { osConfig + , feature + , featureCfg + , userName + , self ? null + }: + if feature.dotfiles == null then { } + else + let + dotfiles = feature.dotfiles; + path = dotfiles.path; + source = dotfiles.source; + target = dotfiles.target or null; + + isLive = + if (featureCfg.dotfilesLive or null) != null then featureCfg.dotfilesLive + else if (dotfiles.live or null) != null then dotfiles.live + else (osConfig.dotfiles.live or false); + + targetPath = if target != null then target else "~/.config/${path}"; + isConfigDir = lib.hasPrefix "~/.config/" targetPath || lib.hasPrefix ".config/" targetPath; + configPath = + if isConfigDir then + let withoutTilde = lib.removePrefix "~/.config/" targetPath; in + lib.removePrefix ".config/" withoutTilde + else null; + + homePath = + if !isConfigDir then + if lib.hasPrefix "~/" targetPath then lib.removePrefix "~/" targetPath + else targetPath + else null; + in + { config, lib, pkgs, ... }: + let + isDarwin = pkgs.stdenv.hostPlatform.isDarwin; + defaultRepoPath = + if isDarwin then "/Users/${userName}/dot" + else "/home/${userName}/dot"; + envDotfilesDir = osConfig.environment.variables.DOTFILES_DIR or null; + repoPath = + if envDotfilesDir != null + && (lib.hasPrefix "/home/${userName}/" envDotfilesDir || lib.hasPrefix "/Users/${userName}/" envDotfilesDir) + then envDotfilesDir + else defaultRepoPath; + + sourcePath = + if isLive then "${repoPath}/${source}" + else if self != null then "${self}/${source}" + else throw "self must be passed to compileUserDotfiles for Nix store dotfiles mode."; + + symlink = + if isLive then config.lib.file.mkOutOfStoreSymlink sourcePath + else sourcePath; + in + if isConfigDir then { + xdg.configFile.${configPath}.source = symlink; + } else { + home.file.${homePath}.source = symlink; + }; + + # Dynamic per-user Home Manager module compilation + # Compiles each user's enabled features into native Home Manager module imports + compileUserHomeModules = + { osConfig + , pkgs + , userName + , userCfg + , featureCatalog ? [ ] + , self ? null + , inputs ? { } + , modulesRegistry ? null + }: + let + isDarwinHost = pkgs.stdenv.hostPlatform.isDarwin; + isLinuxHost = !isDarwinHost; + + enabledFeatures = resolveUserFeatures { + inherit osConfig pkgs userName userCfg featureCatalog modulesRegistry; + hostPlatform = if isDarwinHost then "darwin" else "linux"; + }; + + compileSingleFeature = feature: + let + pathParts = lib.splitString "." feature.path; + globalCfg = lib.foldl' (acc: p: acc.${p} or { }) (osConfig.modules or { }) pathParts; + userRawCfg = lib.foldl' (acc: p: acc.${p} or { }) (userCfg.modules or { }) pathParts; + featureCfg = lib.recursiveUpdate globalCfg userRawCfg; + + dotfilesConfig = compileUserDotfiles { + inherit osConfig feature featureCfg userName self; + }; + in + { config, pkgs, lib, ... }@hmArgs: + let + featureArgs = hmArgs // { + inherit featureCfg userName osConfig; + }; + in + lib.mkMerge [ + (if feature.homeModule != null then + (if lib.isFunction feature.homeModule then feature.homeModule featureArgs else feature.homeModule) + else { }) + (if isLinuxHost && feature.homeModuleLinux != null then + (if lib.isFunction feature.homeModuleLinux then feature.homeModuleLinux featureArgs else feature.homeModuleLinux) + else { }) + (if isDarwinHost && feature.homeModuleDarwin != null then + (if lib.isFunction feature.homeModuleDarwin then feature.homeModuleDarwin featureArgs else feature.homeModuleDarwin) + else { }) + (if lib.isFunction dotfilesConfig then dotfilesConfig hmArgs else dotfilesConfig) + ]; + in + map compileSingleFeature enabledFeatures; + + # Two-stage system activation + # Evaluates nixosModule / darwinModule once at machine level when selectedUsers != [] or globally enabled + compileSystemFeatureModules = + { config + , pkgs + , lib + , system + , featureCatalog ? [ ] + , modulesRegistry ? null + }: + let + isLinux = system == "x86_64-linux" || system == "aarch64-linux"; + isDarwin = system == "aarch64-darwin" || system == "x86_64-darwin"; + allUserNames = lib.attrNames (lib.filterAttrs (_: u: u.enable) (config.hostUsers or { })); + + catalog = + if featureCatalog != [ ] then featureCatalog + else if modulesRegistry != null then (modulesRegistry.features or modulesRegistry.featureCatalog or [ ]) + else if config ? _modulesRegistry && config._modulesRegistry != null then (config._modulesRegistry.features or config._modulesRegistry.featureCatalog or [ ]) + else [ ]; + in + lib.mkMerge (map + (feature: + let + pathParts = lib.splitString "." feature.path; + globalCfg = lib.foldl' (acc: p: acc.${p} or { }) (config.modules or { }) pathParts; + + selectedUsers = lib.filter + (u: + isFeatureEnabledForUser { + inherit config system modulesRegistry; + userName = u; + featurePath = feature.path; + featureCatalog = catalog; + } + ) + allUserNames; + + isGloballyEnabled = (globalCfg.enable or null) == true; + shouldActivate = (selectedUsers != [ ]) || isGloballyEnabled; + + systemArgs = { + inherit config pkgs lib system selectedUsers; + featureCfg = globalCfg; + }; + in + lib.mkIf shouldActivate ( + lib.mkMerge [ + (if isLinux && feature.nixosModule != null then + (if lib.isFunction feature.nixosModule then feature.nixosModule systemArgs else feature.nixosModule) + else { }) + (if isDarwin && feature.darwinModule != null then + (if lib.isFunction feature.darwinModule then feature.darwinModule systemArgs else feature.darwinModule) + else { }) + ] + ) + ) + catalog); + +in +{ + inherit + computeModulePath + mkFeature + buildFeatureOptionTree + isFeatureEnabledForUser + resolveUserFeatures + compileUserDotfiles + compileUserHomeModules + compileSystemFeatureModules; +} diff --git a/lib/flakeHelpers.nix b/lib/flakeHelpers.nix index 172aec08..02633d50 100644 --- a/lib/flakeHelpers.nix +++ b/lib/flakeHelpers.nix @@ -5,23 +5,28 @@ let inherit (inputs) nixpkgs nix-darwin home-manager vscode-server hardware charon-key sops-nix; - # Helper to wrap a module file and filter out 'meta' attribute - # NixOS modules don't allow arbitrary top-level attributes - # Accept all possible module arguments and forward them - # computedModulePath is passed for modules modules + # Helper to wrap a module file and pass computed _modulePath + # Features in modules/home are compiled centrally by the feature compiler in user.nix, returning { } + # System modules in modules/systems are passed through directly wrapModuleFile = filePath: computedModulePath: { config ? null, options ? null, pkgs ? null, lib ? null, system ? null, inputs ? null, overlays ? null, home-manager ? null, hardware ? null, nixpkgs ? null, ... }@args: let # Import the actual module with all arguments plus computed path - originalModule = import filePath (args // { _modulePath = computedModulePath; }); - # Filter out meta - keep everything else - filtered = lib.filterAttrs (name: _: name != "meta") originalModule; + imported = import filePath; + originalModule = + if lib.isFunction imported + then imported (args // { _modulePath = computedModulePath; }) + else imported; in - filtered; + if (originalModule._type or "") == "feature" then + # Features are handled centrally by the feature compiler in user.nix + { } + else + originalModule; # Compute module path from file path for modules # e.g., "/path/to/modules/home/cli/htop.nix" -> "home.cli.htop" # e.g., "/path/to/modules/home/cli/htop/htop.nix" -> "home.cli.htop" (dedup) - computeModulePath = filePath: + computeModulePath = lib.my.computeModulePath or (filePath: let pathStr = toString filePath; # Check if this is a modules module @@ -40,7 +45,7 @@ let else parts; modulePath = lib.concatStringsSep "." dedupedParts; in - if isModulePath then modulePath else null; + if isModulePath then modulePath else null); # Process module directories (like modules/home) that have a default.nix # exporting a .modules list @@ -76,7 +81,9 @@ let moduleFiles; # Build registry during import time (not module evaluation time) - modulesRegistry = (import ../modules/_core/registry.nix { inherit lib; }).moduleRegistry or { modules = [ ]; }; + modulesRegistry = (import ../modules/_core/registry.nix { inherit lib inputs; }).moduleRegistry or { modules = [ ]; }; + + privateExtension = inputs.private-extension.dotExtension or { }; in { @@ -100,6 +107,8 @@ in # antigravity-nix exposes only packages/overlays (no darwinModules); # google-antigravity* are injected via overlays/additions.nix ] + ++ (privateExtension.systemModules.all or [ ]) + ++ (privateExtension.systemModules.darwin or [ ]) ++ mkConfigurationModules [ ../modules/home ] @@ -132,6 +141,8 @@ in charon-key.nixosModules.default sops-nix.nixosModules.sops ] + ++ (privateExtension.systemModules.all or [ ]) + ++ (privateExtension.systemModules.linux or [ ]) ++ mkConfigurationModules [ ../modules/home ] diff --git a/modules/_core/registry.nix b/modules/_core/registry.nix index ad8017cb..97f1d07f 100644 --- a/modules/_core/registry.nix +++ b/modules/_core/registry.nix @@ -1,16 +1,22 @@ -# Build module registry from modules directory +# Build module registry and catalog features from modules directory and private-extension # This is called during import time, not module evaluation time -{ lib }: +{ lib, inputs ? { } }: let modulesDir = ../home; - # Use library function to build registry from the modules directory - registry = lib.my.buildModuleRegistry modulesDir "home"; + extFeatures = inputs.private-extension.dotExtension.features or [ ]; + + # Use library function to build registry and feature catalog + registry = lib.my.buildModuleRegistry modulesDir "home" extFeatures; in { # List of module files to import modules = map (m: m.file) registry.allModules; - # Registry metadata for tags.nix + # Registry metadata moduleRegistry = registry; + + # Feature catalog + features = registry.features or [ ]; + featureCatalog = registry.featureCatalog or [ ]; } diff --git a/modules/_core/user.nix b/modules/_core/user.nix index 4782755b..4936a446 100644 --- a/modules/_core/user.nix +++ b/modules/_core/user.nix @@ -1,6 +1,6 @@ # Core user module - defines hostUsers options # This is a foundational module, not a typical package module -{ config, lib, system, modulesRegistry ? null, ... }: +{ config, lib, system, pkgs, modulesRegistry ? null, self ? null, inputs ? { }, ... }@args: with lib; @@ -12,14 +12,18 @@ let # Get enabled users enabledUsers = filterAttrs (name: cfg: cfg.enable) config.hostUsers; - # Build module options structure from registry - # Creates options like hostUsers..modules..enable - # Uses attrs to allow nested paths like home.media.vlc.enable = true - # types.attrs is intentional — generating typed options per module path would - # require a deeply nested option set from the registry. The registry metadata - # is used for requires/dependency resolution in shouldEnableModule instead. - buildModuleOptions = _registry: - types.attrs; + # Feature catalog and option generation + featureCatalog = + if modulesRegistry != null then (modulesRegistry.features or modulesRegistry.featureCatalog or [ ]) + else [ ]; + allKnownModulePaths = + if modulesRegistry != null then map (m: m.path) (modulesRegistry.allModules or modulesRegistry.modules or [ ]) + else [ ]; + + generatedFeatureOptions = lib.my.buildFeatureOptionTree { + inherit featureCatalog; + knownModulePaths = allKnownModulePaths; + }; # SSH key submodule keyModule = types.submodule { @@ -183,7 +187,14 @@ let # Per-user module configuration (hierarchical enables) # Enable at any level: home.browsers.enable (all browsers) or home.browsers.vivaldi.enable (specific) modules = mkOption { - type = buildModuleOptions modulesRegistry; + type = types.submoduleWith { + modules = [ + { + freeformType = types.attrsOf types.anything; + options = generatedFeatureOptions; + } + ]; + }; default = { }; description = '' Module configuration for this user. @@ -215,6 +226,14 @@ let in { + options.modules = generatedFeatureOptions; + + options._modulesRegistry = mkOption { + internal = true; + default = modulesRegistry; + description = "Internal reference to modulesRegistry"; + }; + options.hostUsers = mkOption { type = types.attrsOf userModule; default = { }; @@ -257,79 +276,92 @@ in ''; }; - # Create system users from enabled hostUsers - config = mkIf (enabledUsers != { }) ({ - # Create users.users entries for each enabled hostUser - # Note: Darwin users are managed differently - only set Linux-specific options on Linux - users.users = - let - # Generate UIDs starting from 1000, incrementing for each user - # This ensures unique UIDs for multiple users - userNames = attrNames enabledUsers; - # Helper to create UID mapping: first user gets 1000, second gets 1001, etc. - createUidMap = names: idx: - if names == [ ] then { } - else - (createUidMap (tail names) (idx + 1)) // { - ${head names} = 1000 + idx; - }; - uidMap = createUidMap userNames 0; - in - mapAttrs - (name: cfg: - if isLinux then { - name = name; - isNormalUser = true; - home = "/home/${name}"; - group = "users"; - uid = if cfg.uid != null then cfg.uid else uidMap.${name}; - extraGroups = cfg.extraGroups; - } else { - # Darwin: minimal user config, home-manager handles the rest - name = name; - home = "/Users/${name}"; - } - ) - enabledUsers; + # Create system users from enabled hostUsers and compile features + config = mkMerge [ + (mkIf (enabledUsers != { }) ({ + # Create users.users entries for each enabled hostUser + # Note: Darwin users are managed differently - only set Linux-specific options on Linux + users.users = + let + # Generate UIDs starting from 1000, incrementing for each user + # This ensures unique UIDs for multiple users + userNames = attrNames enabledUsers; + # Helper to create UID mapping: first user gets 1000, second gets 1001, etc. + createUidMap = names: idx: + if names == [ ] then { } + else + (createUidMap (tail names) (idx + 1)) // { + ${head names} = 1000 + idx; + }; + uidMap = createUidMap userNames 0; + in + mapAttrs + (name: cfg: + if isLinux then { + name = name; + isNormalUser = true; + home = "/home/${name}"; + group = "users"; + uid = if cfg.uid != null then cfg.uid else uidMap.${name}; + extraGroups = cfg.extraGroups; + } else { + # Darwin: minimal user config, home-manager handles the rest + name = name; + home = "/Users/${name}"; + } + ) + enabledUsers; - # Add users to nix trusted/allowed users - nix.settings = { - trusted-users = [ "root" ] ++ (attrNames enabledUsers); - allowed-users = [ "root" ] ++ (attrNames enabledUsers); - }; + # Add users to nix trusted/allowed users + nix.settings = { + trusted-users = [ "root" ] ++ (attrNames enabledUsers); + allowed-users = [ "root" ] ++ (attrNames enabledUsers); + }; - # Configure home-manager for each enabled user - home-manager = { - useUserPackages = true; - useGlobalPkgs = true; - # Back up existing files instead of failing - backupFileExtension = "backup"; + # Configure home-manager for each enabled user + home-manager = { + useUserPackages = true; + useGlobalPkgs = true; + # Back up existing files instead of failing + backupFileExtension = "backup"; - users = mapAttrs - (name: cfg: { - home = { - stateVersion = "26.05"; - username = name; - homeDirectory = if isDarwin then "/Users/${name}" else "/home/${name}"; - }; - # Programs and packages will be configured by modules via per-user tags (Deliverable 5) - }) - enabledUsers; - }; - } // optionalAttrs isDarwin { - # Darwin requires primaryUser for certain options - system.primaryUser = head (attrNames enabledUsers); - } // optionalAttrs isLinux { - # Grant NOPASSWD sudo to any user that opted in - security.sudo.extraRules = - let - nopasswdUsers = filterAttrs (_: cfg: cfg.enable && cfg.sudo.nopasswd) config.hostUsers; - in - mapAttrsToList - (name: _: { - users = [ name ]; - commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }]; - }) - nopasswdUsers; - }); + users = mapAttrs + (userName: userCfg: + hmArgs: { + imports = lib.my.compileUserHomeModules { + osConfig = hmArgs.osConfig or config; + inherit pkgs userName userCfg featureCatalog modulesRegistry; + self = args.self or null; + inputs = args.inputs or { }; + }; + home = { + stateVersion = "26.05"; + username = userName; + homeDirectory = if isDarwin then "/Users/${userName}" else "/home/${userName}"; + }; + } + ) + enabledUsers; + }; + } // optionalAttrs isDarwin { + # Darwin requires primaryUser for certain options + system.primaryUser = head (attrNames enabledUsers); + } // optionalAttrs isLinux { + # Grant NOPASSWD sudo to any user that opted in + security.sudo.extraRules = + let + nopasswdUsers = filterAttrs (_: cfg: cfg.enable && cfg.sudo.nopasswd) config.hostUsers; + in + mapAttrsToList + (name: _: { + users = [ name ]; + commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }]; + }) + nopasswdUsers; + })) + + (lib.my.compileSystemFeatureModules { + inherit config pkgs lib system featureCatalog modulesRegistry; + }) + ]; } diff --git a/modules/home/ai-tools/claude.nix b/modules/home/ai-tools/claude.nix index 43b59f16..dfc8e3b1 100644 --- a/modules/home/ai-tools/claude.nix +++ b/modules/home/ai-tools/claude.nix @@ -1,10 +1,9 @@ -# Telegram messenger -{ lib, pkgs, ... }@args: +# Claude Code CLI tool +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; - description = "Telegram desktop messenger"; - module = { - allSystems.home.packages = [ pkgs.claude-code ]; +lib.my.mkFeature { + description = "Claude Code CLI tool"; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.claude-code ]; }; } diff --git a/modules/home/ai-tools/codex.nix b/modules/home/ai-tools/codex.nix index 403f67eb..7447da34 100644 --- a/modules/home/ai-tools/codex.nix +++ b/modules/home/ai-tools/codex.nix @@ -1,11 +1,10 @@ # Codex - OpenAI's AI coding agent -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Codex - OpenAI's AI coding agent CLI"; - module = { - allSystems.programs.codex = { + homeModule = _: { + programs.codex = { enable = true; }; }; diff --git a/modules/home/ai-tools/gemini-cli.nix b/modules/home/ai-tools/gemini-cli.nix index 08d43678..48755a5d 100644 --- a/modules/home/ai-tools/gemini-cli.nix +++ b/modules/home/ai-tools/gemini-cli.nix @@ -1,14 +1,11 @@ # Gemini CLI - Google's AI coding assistant -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Gemini CLI - Google's AI coding assistant"; - module = { - allSystems = { - programs.antigravity-cli = { - enable = true; - }; + homeModule = _: { + programs.antigravity-cli = { + enable = true; }; }; } diff --git a/modules/home/browsers/chromium.nix b/modules/home/browsers/chromium.nix index cf8baa22..117d94fd 100644 --- a/modules/home/browsers/chromium.nix +++ b/modules/home/browsers/chromium.nix @@ -1,39 +1,35 @@ # Chromium browser - cross-platform # On Darwin, package = null means use externally installed Chromium (homebrew cask) -{ lib, pkgs, ... }@args: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +{ lib, ... }: + +lib.my.mkFeature { description = "Chromium web browser"; - module = { - # Darwin: manage config for externally-installed Chromium - darwinSystems = { - programs.chromium = { - enable = true; - package = null; # Install Chromium via: brew install --cask chromium - }; + homeModuleDarwin = _: { + programs.chromium = { + enable = true; + package = null; # Install Chromium via: brew install --cask chromium }; - # Linux: use nixpkgs chromium package - nixosSystems = { - programs.chromium = { - enable = true; - commandLineArgs = [ - "--ignore-gpu-blocklist" - "--enable-gpu-rasterization" - "--enable-zero-copy" - "--enable-features=VaapiVideoDecoder,VaapiVideoEncoder" - "--enable-accelerated-video-decode" - "--ozone-platform-hint=auto" - ]; - }; - xdg.desktopEntries.chromium = { - name = "Chromium"; - genericName = "Web Browser"; - exec = "chromium %U"; - icon = "chromium"; - terminal = false; - categories = [ "Network" "WebBrowser" ]; - mimeType = [ "text/html" "text/xml" "application/xhtml+xml" "x-scheme-handler/http" "x-scheme-handler/https" ]; - }; + }; + homeModuleLinux = _: { + programs.chromium = { + enable = true; + commandLineArgs = [ + "--ignore-gpu-blocklist" + "--enable-gpu-rasterization" + "--enable-zero-copy" + "--enable-features=VaapiVideoDecoder,VaapiVideoEncoder" + "--enable-accelerated-video-decode" + "--ozone-platform-hint=auto" + ]; + }; + xdg.desktopEntries.chromium = { + name = "Chromium"; + genericName = "Web Browser"; + exec = "chromium %U"; + icon = "chromium"; + terminal = false; + categories = [ "Network" "WebBrowser" ]; + mimeType = [ "text/html" "text/xml" "application/xhtml+xml" "x-scheme-handler/http" "x-scheme-handler/https" ]; }; }; } diff --git a/modules/home/browsers/vivaldi.nix b/modules/home/browsers/vivaldi.nix index 8c73334f..66c1a805 100644 --- a/modules/home/browsers/vivaldi.nix +++ b/modules/home/browsers/vivaldi.nix @@ -20,7 +20,7 @@ # 2. Rename .crx to .zip, extract, read version from manifest.json # 3. Use crxPath + version in extensions config # ============================================================================= -{ lib, pkgs, system, ... }@args: +{ lib, ... }: let # Shared extensions config (used by both platforms) # Format: "id" or { id = "..."; } or { id = "..."; updateUrl = "..."; } @@ -52,73 +52,75 @@ let # } ]; in -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" "darwin" ]; description = "Vivaldi web browser with extensions"; - module = { - # ------------------------------------------------------------------------- - # Darwin: homebrew cask + package = null (MUST set package before enable) - # Note: pkgs.vivaldi is Linux-only, so we MUST set package = null - # ------------------------------------------------------------------------- - darwinSystems = { - programs.vivaldi = { - enable = true; - package = null; # Install via: brew install --cask vivaldi - extensions = sharedExtensions; - }; - homebrew.casks = [ "vivaldi" ]; + # Shared across Darwin and Linux + homeModule = { + programs.vivaldi = { + enable = true; + extensions = sharedExtensions; }; + }; - # ------------------------------------------------------------------------- - # Linux: nixpkgs package + performance flags + codecs - # ------------------------------------------------------------------------- - nixosSystems = { - programs.vivaldi = { - enable = true; - extensions = sharedExtensions; + # ------------------------------------------------------------------------- + # Darwin: homebrew cask + package = null (MUST set package before enable) + # Note: pkgs.vivaldi is Linux-only, so we MUST set package = null + # ------------------------------------------------------------------------- + darwinModule = { + homebrew.casks = [ "vivaldi" ]; + }; - commandLineArgs = [ - "--ignore-gpu-blocklist" - "--enable-gpu-rasterization" - "--enable-zero-copy" - "--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,UseHDRTransferFunction,WebRTCPipeWireCapturer" - "--enable-accelerated-video-decode" - "--ozone-platform-hint=auto" - "--force-device-scale-factor=1.25" - "--use-pulseaudio" - # "--enable-logging=stderr" # Uncomment for debugging - ]; + homeModuleDarwin = { + programs.vivaldi.package = null; # Install via: brew install --cask vivaldi + }; - # Spell-check dictionaries (Linux only - Darwin uses system dictionaries) - dictionaries = [ - pkgs.hunspellDictsChromium.en_US - # pkgs.hunspellDictsChromium.de_DE # German - # pkgs.hunspellDictsChromium.ru_RU # Russian - ]; - }; + # ------------------------------------------------------------------------- + # Linux: nixpkgs package + performance flags + codecs + # ------------------------------------------------------------------------- + homeModuleLinux = { pkgs, ... }: { + programs.vivaldi = { + commandLineArgs = [ + "--ignore-gpu-blocklist" + "--enable-gpu-rasterization" + "--enable-zero-copy" + "--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,UseHDRTransferFunction,WebRTCPipeWireCapturer" + "--enable-accelerated-video-decode" + "--ozone-platform-hint=auto" + "--force-device-scale-factor=1.25" + "--use-pulseaudio" + # "--enable-logging=stderr" # Uncomment for debugging + ]; - # Proprietary codecs (H.264, AAC) - separate package - home.packages = [ - pkgs.vivaldi-ffmpeg-codecs + # Spell-check dictionaries (Linux only - Darwin uses system dictionaries) + dictionaries = [ + pkgs.hunspellDictsChromium.en_US + # pkgs.hunspellDictsChromium.de_DE # German + # pkgs.hunspellDictsChromium.ru_RU # Russian ]; + }; + + # Proprietary codecs (H.264, AAC) - separate package + home.packages = [ + pkgs.vivaldi-ffmpeg-codecs + ]; - # Desktop entry for application launchers - xdg.desktopEntries.vivaldi = { - name = "Vivaldi"; - genericName = "Web Browser"; - exec = "vivaldi %U"; - icon = "vivaldi"; - terminal = false; - categories = [ "Network" "WebBrowser" ]; - mimeType = [ - "text/html" - "text/xml" - "application/xhtml+xml" - "x-scheme-handler/http" - "x-scheme-handler/https" - ]; - }; + # Desktop entry for application launchers + xdg.desktopEntries.vivaldi = { + name = "Vivaldi"; + genericName = "Web Browser"; + exec = "vivaldi %U"; + icon = "vivaldi"; + terminal = false; + categories = [ "Network" "WebBrowser" ]; + mimeType = [ + "text/html" + "text/xml" + "application/xhtml+xml" + "x-scheme-handler/http" + "x-scheme-handler/https" + ]; }; }; } diff --git a/modules/home/cli/gcloud.nix b/modules/home/cli/gcloud.nix index 98dfc287..98919a14 100644 --- a/modules/home/cli/gcloud.nix +++ b/modules/home/cli/gcloud.nix @@ -1,10 +1,10 @@ # Google Cloud SDK (gcloud CLI) -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "darwin" ]; description = "Google Cloud SDK (gcloud, gsutil, bq)"; - module = { - darwinSystems.home.packages = [ pkgs.google-cloud-sdk ]; + homeModuleDarwin = { pkgs, ... }: { + home.packages = [ pkgs.google-cloud-sdk ]; }; } diff --git a/modules/home/cli/htop.nix b/modules/home/cli/htop.nix index f366d484..1d5f6406 100644 --- a/modules/home/cli/htop.nix +++ b/modules/home/cli/htop.nix @@ -1,9 +1,9 @@ # htop - interactive process viewer -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "htop - interactive process viewer"; - module = { - allSystems.home.packages = [ pkgs.htop ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.htop ]; }; } diff --git a/modules/home/cli/shell-utils.nix b/modules/home/cli/shell-utils.nix index d9cb2035..9967370b 100644 --- a/modules/home/cli/shell-utils.nix +++ b/modules/home/cli/shell-utils.nix @@ -1,22 +1,19 @@ # Essential shell utilities -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Essential shell utilities (bat, fzf, wget, direnv)"; - module = { - allSystems = { - home.packages = with pkgs; [ - bat - fzf - fastfetch - wget - ]; - programs.direnv = { - enable = true; - nix-direnv.enable = true; - silent = true; # Silence all direnv output - }; + homeModule = { pkgs, ... }: { + home.packages = with pkgs; [ + bat + fzf + fastfetch + wget + ]; + programs.direnv = { + enable = true; + nix-direnv.enable = true; + silent = true; }; }; } diff --git a/modules/home/cli/vault.nix b/modules/home/cli/vault.nix index 79e8cbad..7f91db9c 100644 --- a/modules/home/cli/vault.nix +++ b/modules/home/cli/vault.nix @@ -1,9 +1,9 @@ # HashiCorp Vault CLI -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "HashiCorp Vault CLI"; - module = { - allSystems.home.packages = [ pkgs.vault ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.vault ]; }; } diff --git a/modules/home/desktop/appearance/cursor.nix b/modules/home/desktop/appearance/cursor.nix index 14f1f358..8435e0af 100644 --- a/modules/home/desktop/appearance/cursor.nix +++ b/modules/home/desktop/appearance/cursor.nix @@ -1,11 +1,11 @@ # Cursor theme and settings -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Cursor theme (WhiteSur) and dconf settings utility"; - module = { - nixosSystems.home.packages = with pkgs; [ + homeModuleLinux = { pkgs, ... }: { + home.packages = with pkgs; [ whitesur-cursors dconf ]; diff --git a/modules/home/desktop/awesomewm/awesomewm.nix b/modules/home/desktop/awesomewm/awesomewm.nix index 115a6409..c9d6186d 100644 --- a/modules/home/desktop/awesomewm/awesomewm.nix +++ b/modules/home/desktop/awesomewm/awesomewm.nix @@ -1,54 +1,52 @@ # AwesomeWM - X11 window manager -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "AwesomeWM X11 window manager"; # System-level configuration (requires root) - systemModule = { - nixosSystems = { - # X server with AwesomeWM - services.xserver = { - enable = true; + nixosModule = { pkgs, ... }: { + # X server with AwesomeWM + services.xserver = { + enable = true; - windowManager.awesome = { - enable = true; - luaModules = [ pkgs.luaPackages.lgi ]; - }; + windowManager.awesome = { + enable = true; + luaModules = [ pkgs.luaPackages.lgi ]; + }; - displayManager.lightdm.enable = true; + displayManager.lightdm.enable = true; - # Keyboard layout - xkb = { - layout = "us,ru"; - variant = ""; - options = "grp:alt_space_toggle"; - }; + # Keyboard layout + xkb = { + layout = "us,ru"; + variant = ""; + options = "grp:alt_space_toggle"; }; + }; - services.displayManager.defaultSession = "none+awesome"; - - # Picom compositor for tear-free rendering - services.picom = { - enable = true; - backend = "glx"; - vSync = true; - settings = { - glx-no-stencil = true; - glx-no-rebind-pixmap = true; - use-damage = true; - unredir-if-possible = false; - glx-swap-method = 2; - vsync = true; - }; + services.displayManager.defaultSession = "none+awesome"; + + # Picom compositor for tear-free rendering + services.picom = { + enable = true; + backend = "glx"; + vSync = true; + settings = { + glx-no-stencil = true; + glx-no-rebind-pixmap = true; + use-damage = true; + unredir-if-possible = false; + glx-swap-method = 2; + vsync = true; }; }; }; # User-level configuration (routed to home-manager.users.*) - module = { - nixosSystems.home.packages = with pkgs; [ + homeModuleLinux = { pkgs, ... }: { + home.packages = with pkgs; [ rofi xclip ]; diff --git a/modules/home/desktop/gnome/gnome.nix b/modules/home/desktop/gnome/gnome.nix deleted file mode 100644 index db44603b..00000000 --- a/modules/home/desktop/gnome/gnome.nix +++ /dev/null @@ -1,45 +0,0 @@ -# GNOME desktop environment -{ lib, pkgs, ... }@args: - -lib.my.mkModuleV2 args { - platforms = [ "linux" ]; - description = "GNOME desktop environment"; - - systemModule = { - nixosSystems = { - # X server with GDM and GNOME - - services = { - displayManager.gdm.enable = true; - desktopManager.gnome.enable = true; - - xserver = { - enable = true; - - # Keyboard layout - xkb = { - layout = "us,ru"; - variant = ",mac"; - options = "grp:alt_space_toggle"; - }; - }; - }; - - # Disable GNOME SSH agent (use system ssh-agent) - services.gnome.gcr-ssh-agent.enable = false; - - # Console keyboard layout - console.keyMap = "us"; - }; - }; - - # User-level GNOME utilities (nautilus lives in utils/nautilus.nix) - module = { - nixosSystems = { - home.packages = with pkgs; [ - gnome-tweaks - gnome-extension-manager - ]; - }; - }; -} diff --git a/modules/home/desktop/hyprland/hyprland.nix b/modules/home/desktop/hyprland/hyprland.nix index af1c25b5..7e86481f 100644 --- a/modules/home/desktop/hyprland/hyprland.nix +++ b/modules/home/desktop/hyprland/hyprland.nix @@ -1,110 +1,95 @@ # Hyprland Wayland compositor -{ lib, config, pkgs, ... }@args: +{ lib, ... }: -let - # Strip "#" — Hyprland color values use rgba(rrggbbaa) - hex = c: lib.removePrefix "#" c; - rgba = color: alpha: "rgba(${hex color}${alpha})"; - - theme = lib.my.getTheme config; - r = theme.roles; - - # Generate a Lua theme module required by hyprland.lua - # Returns a table with color values that hyprland.lua uses. - hyprTheme = '' - -- Generated by Nix — do not edit. - -- Change theme by updating theme.name in your host config. - -- Theme: ${theme.displayName} - return { - border_active = "${rgba r.borderFocused "ee"}", - border_inactive = "${rgba r.border "66"}", - shadow_color = "${rgba r.background "cc"}", - shadow_inactive = "${rgba r.background "99"}", - } - ''; - -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Hyprland Wayland compositor"; # System-level configuration (requires root) - systemModule = { - nixosSystems = { - # Enable Wayland and Hyprland - programs.hyprland = { - enable = true; - withUWSM = true; - xwayland.enable = true; - }; + nixosModule = { pkgs, lib, ... }: { + # Enable Wayland and Hyprland + programs.hyprland = { + enable = true; + withUWSM = true; + xwayland.enable = true; + }; - # Display Manager for Wayland - services.greetd = { - enable = true; - settings = { - default_session = { - command = "${pkgs.tuigreet}/bin/tuigreet --time --cmd 'uwsm start hyprland-uwsm.desktop'"; - user = "greeter"; - }; + # Display Manager for Wayland + services.greetd = { + enable = true; + settings = { + default_session = { + command = "${pkgs.tuigreet}/bin/tuigreet --time --cmd 'uwsm start hyprland-uwsm.desktop'"; + user = "greeter"; }; }; + }; - # Unlock gnome-keyring at login via PAM so tuigreet auto-unlocks it - # using the user's login password — no separate keyring password prompt. - security.pam.services.greetd.enableGnomeKeyring = true; + # Unlock gnome-keyring at login via PAM so tuigreet auto-unlocks it + # using the user's login password — no separate keyring password prompt. + security.pam.services.greetd.enableGnomeKeyring = true; - # XDG portal for screen sharing and file dialogs - xdg.portal = { - enable = true; - extraPortals = with pkgs; [ - xdg-desktop-portal-hyprland - xdg-desktop-portal-gtk - ]; - }; + # XDG portal for screen sharing and file dialogs + xdg.portal = { + enable = true; + extraPortals = with pkgs; [ + xdg-desktop-portal-hyprland + xdg-desktop-portal-gtk + ]; + }; - # Keyboard layout (for Wayland) - services.xserver = { - enable = lib.mkDefault false; - xkb = { - layout = "us,ru"; - variant = ",mac"; - options = "grp:alt_space_toggle"; - }; + # Keyboard layout (for Wayland) + services.xserver = { + enable = lib.mkDefault false; + xkb = { + layout = "us,ru"; + variant = ",mac"; + options = "grp:alt_space_toggle"; }; + }; - # Console keyboard layout - console.keyMap = "us"; + # Console keyboard layout + console.keyMap = "us"; - # Fonts for Waybar and other UI elements - fonts.packages = with pkgs; [ - font-awesome - nerd-fonts.jetbrains-mono - nerd-fonts.fira-code - ]; - }; + # Fonts for Waybar and other UI elements + fonts.packages = with pkgs; [ + font-awesome + nerd-fonts.jetbrains-mono + nerd-fonts.fira-code + ]; }; - module = { - nixosSystems = { + homeModuleLinux = { pkgs, osConfig, lib, ... }: + let + hex = c: lib.removePrefix "#" c; + rgba = color: alpha: "rgba(${hex color}${alpha})"; + theme = lib.my.getTheme osConfig; + r = theme.roles; + + hyprTheme = '' + -- Generated by Nix — do not edit. + -- Change theme by updating theme.name in your host config. + -- Theme: ${theme.displayName} + return { + border_active = "${rgba r.borderFocused "ee"}", + border_inactive = "${rgba r.border "66"}", + shadow_color = "${rgba r.background "cc"}", + shadow_inactive = "${rgba r.background "99"}", + } + ''; + in + { # Start gnome-keyring daemon as a user systemd service. - # Provides the Secret Service D-Bus API that Electron apps use - # to store credentials (VS Code, Antigravity IDE, Chrome, etc.). services.gnome-keyring = { enable = true; components = [ "secrets" "ssh" ]; }; - # gcr provides the GUI unlock prompter (fallback if PAM didn't unlock) home.packages = [ pkgs.gcr_4 pkgs.grim pkgs.slurp ]; - # Generated Hyprland theme module (Lua). - # Written as a sibling to the hypr/ symlink (same pattern as waybar-theme.css). - # hyprland.lua loads it with dofile() to pick up color values. xdg.configFile."hypr-theme.lua".text = hyprTheme; }; - }; - # Dotfiles symlink (live-editable) dotfiles = { path = "hypr"; source = "modules/home/desktop/hyprland/dotfiles"; diff --git a/modules/home/desktop/services/audio.nix b/modules/home/desktop/services/audio.nix index e38a97e8..4869f453 100644 --- a/modules/home/desktop/services/audio.nix +++ b/modules/home/desktop/services/audio.nix @@ -1,13 +1,14 @@ # Audio control utilities -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; - description = "Audio control utilities (pamixer, pavucontrol)"; - module = { - nixosSystems.home.packages = with pkgs; [ + description = "Audio control utilities (pamixer, pavucontrol, pulseaudio)"; + homeModuleLinux = { pkgs, ... }: { + home.packages = with pkgs; [ pamixer pavucontrol + pulseaudio ]; }; } diff --git a/modules/home/desktop/services/brightness.nix b/modules/home/desktop/services/brightness.nix index 0b9a87f6..171e6d3d 100644 --- a/modules/home/desktop/services/brightness.nix +++ b/modules/home/desktop/services/brightness.nix @@ -1,10 +1,10 @@ # Brightness control -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Brightness control (brightnessctl)"; - module = { - nixosSystems.home.packages = [ pkgs.brightnessctl ]; + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.brightnessctl ]; }; } diff --git a/modules/home/desktop/services/notifications.nix b/modules/home/desktop/services/notifications.nix index 8592f9c1..81b91688 100644 --- a/modules/home/desktop/services/notifications.nix +++ b/modules/home/desktop/services/notifications.nix @@ -1,10 +1,10 @@ # Notification daemon - dunst -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Notification daemon (dunst)"; - module = { - nixosSystems.home.packages = [ pkgs.dunst ]; + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.dunst ]; }; } diff --git a/modules/home/desktop/utils/nautilus.nix b/modules/home/desktop/utils/nautilus.nix index 3007466a..234f119b 100644 --- a/modules/home/desktop/utils/nautilus.nix +++ b/modules/home/desktop/utils/nautilus.nix @@ -1,11 +1,11 @@ # Nautilus file manager (standalone, no full GNOME desktop required) -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Nautilus file manager"; - module = { - nixosSystems.home.packages = with pkgs; [ + homeModuleLinux = { pkgs, ... }: { + home.packages = with pkgs; [ nautilus # gvfs for MTP/SFTP/trash support in Nautilus gvfs diff --git a/modules/home/desktop/utils/wayland.nix b/modules/home/desktop/utils/wayland.nix index 88ef20a3..b78e3d5f 100644 --- a/modules/home/desktop/utils/wayland.nix +++ b/modules/home/desktop/utils/wayland.nix @@ -1,28 +1,22 @@ # Wayland-specific desktop utilities - screenshots, status bar, wallpaper # Only enabled when Wayland compositor (hyprland) is active -{ lib, pkgs, config, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Wayland-specific utilities - screenshots, status bar, wallpaper"; - module = cfg: - let - # Check if Wayland is enabled system-wide - waylandEnabled = config.systemLinux.desktop.hyprland.enable or false; - in - # Only enable if Wayland is enabled system-wide - lib.mkIf waylandEnabled { - nixosSystems.home.packages = with pkgs; [ - # Screenshot utilities - grim - slurp - wl-clipboard + homeModuleLinux = { pkgs, osConfig, lib, ... }: { + home.packages = lib.optionals (osConfig.programs.hyprland.enable or false) (with pkgs; [ + # Screenshot utilities + grim + slurp + wl-clipboard - # Wallpaper daemon - swww + # Wallpaper daemon + swww - # Status bar - waybar - ]; - }; + # Status bar + waybar + ]); + }; } diff --git a/modules/home/desktop/widgets/launcher.nix b/modules/home/desktop/widgets/launcher.nix index 7a5c4ede..c286c60e 100644 --- a/modules/home/desktop/widgets/launcher.nix +++ b/modules/home/desktop/widgets/launcher.nix @@ -1,181 +1,174 @@ # Application launcher — Rofi (Wayland) -{ lib, config, pkgs, ... }@args: - -let - theme = lib.my.getTheme config; - r = theme.roles; - f = theme.fonts; - - # ── Rofi theme file (colors + widget layout) ──────────────────────────── - # Generated from the active Nix theme. Imported by config.rasi. - rofiTheme = '' - /** - * Generated by Nix — do not edit. - * Theme: ${theme.displayName} - * Variant: ${theme.variant} - * Change by setting theme.name in your host config. - */ - - * { - /* ── Palette ─────────────────────────────────────────────────── */ - bg: ${r.background}; - bg-alt: ${r.backgroundAlt}; - bg-float: ${r.backgroundFloat}; - bg-select: ${r.selection}; - - fg: ${r.foreground}; - fg-muted: ${r.foregroundMuted}; - fg-dim: ${r.foregroundDim}; - - accent: ${r.accent}; - accent-alt: ${r.accentAlt}; - clr-error: ${r.error}; - - /* Required Rofi wildcard properties */ - background-color: transparent; - text-color: @fg; - font: "${f.mono} 12"; - } - - window { - background-color: @bg; - border: 2px solid; - border-color: @accent; - border-radius: 12px; - padding: 0px; - width: 600px; - } - - mainbox { - background-color: transparent; - children: [ inputbar, listview ]; - spacing: 0px; - } - - /* ── Search bar ──────────────────────────────────────────────────────── */ - inputbar { - background-color: @bg-alt; - border-radius: 12px 12px 0px 0px; - padding: 12px 16px; - spacing: 8px; - children: [ prompt, entry ]; - } - - prompt { - text-color: @accent; - font: "${f.mono} Bold 12"; - } - - entry { - text-color: @fg; - placeholder: "Search applications…"; - placeholder-color: @fg-dim; - cursor: text; - } - - /* ── Results list ────────────────────────────────────────────────────── */ - listview { - background-color: transparent; - columns: 1; - lines: 8; - scrollbar: false; - spacing: 2px; - padding: 8px; - border-radius: 0px 0px 12px 12px; - } - - element { - background-color: transparent; - border-radius: 8px; - padding: 8px 12px; - spacing: 10px; - children: [ element-icon, element-text ]; - } - - element normal.normal, - element alternate.normal { - background-color: transparent; - text-color: @fg; - } - - element selected.normal { - background-color: @bg-select; - text-color: @fg; - } - - element normal.active, - element alternate.active { - background-color: transparent; - text-color: @accent; - } - - element selected.active { - background-color: @bg-select; - text-color: @accent; - } - - element normal.urgent, - element selected.urgent { - background-color: @bg-select; - text-color: @clr-error; - } - - element-icon { - size: 20px; - vertical-align: 0.5; - } - - element-text { - text-color: inherit; - expand: true; - highlight: bold ${r.accent}; - } - - /* ── Scrollbar (hidden but defined) ──────────────────────────────────── */ - scrollbar { - background-color: @bg-alt; - handle-color: @fg-dim; - handle-width: 4px; - border-radius: 2px; - width: 6px; - } - ''; - - # ── Rofi config (settings + theme import) ─────────────────────────────── - # @import pulls in the generated theme.rasi from the same directory. - rofiConfig = '' - /** - * Rofi configuration — managed by Nix (launcher.nix) - * Theme colors come from @import "theme.rasi" - */ - configuration { - modi: "drun,run"; - show-icons: true; - display-drun: " Apps"; - display-run: " Run"; - drun-display-format: "{name}"; - hover-select: false; - /* Accept entry on primary click */ - me-select-entry: ""; - me-accept-entry: "MousePrimary"; - } - - @import "theme.rasi" - ''; -in -lib.my.mkModuleV2 args { +{ lib, ... }: + +lib.my.mkFeature { platforms = [ "linux" ]; description = "Application launcher (rofi)"; - module = { - nixosSystems = { - # rofi-wayland: native Wayland fork of rofi, required for Hyprland + homeModuleLinux = { pkgs, osConfig, lib, ... }: + let + theme = lib.my.getTheme osConfig; + r = theme.roles; + f = theme.fonts; + + # ── Rofi theme file (colors + widget layout) ──────────────────────────── + rofiTheme = '' + /** + * Generated by Nix — do not edit. + * Theme: ${theme.displayName} + * Variant: ${theme.variant} + * Change by setting theme.name in your host config. + */ + + * { + /* ── Palette ─────────────────────────────────────────────────── */ + bg: ${r.background}; + bg-alt: ${r.backgroundAlt}; + bg-float: ${r.backgroundFloat}; + bg-select: ${r.selection}; + + fg: ${r.foreground}; + fg-muted: ${r.foregroundMuted}; + fg-dim: ${r.foregroundDim}; + + accent: ${r.accent}; + accent-alt: ${r.accentAlt}; + clr-error: ${r.error}; + + /* Required Rofi wildcard properties */ + background-color: transparent; + text-color: @fg; + font: "${f.mono} 12"; + } + + window { + background-color: @bg; + border: 2px solid; + border-color: @accent; + border-radius: 12px; + padding: 0px; + width: 600px; + } + + mainbox { + background-color: transparent; + children: [ inputbar, listview ]; + spacing: 0px; + } + + /* ── Search bar ──────────────────────────────────────────────────────── */ + inputbar { + background-color: @bg-alt; + border-radius: 12px 12px 0px 0px; + padding: 12px 16px; + spacing: 8px; + children: [ prompt, entry ]; + } + + prompt { + text-color: @accent; + font: "${f.mono} Bold 12"; + } + + entry { + text-color: @fg; + placeholder: "Search applications…"; + placeholder-color: @fg-dim; + cursor: text; + } + + /* ── Results list ────────────────────────────────────────────────────── */ + listview { + background-color: transparent; + columns: 1; + lines: 8; + scrollbar: false; + spacing: 2px; + padding: 8px; + border-radius: 0px 0px 12px 12px; + } + + element { + background-color: transparent; + border-radius: 8px; + padding: 8px 12px; + spacing: 10px; + children: [ element-icon, element-text ]; + } + + element normal.normal, + element alternate.normal { + background-color: transparent; + text-color: @fg; + } + + element selected.normal { + background-color: @bg-select; + text-color: @fg; + } + + element normal.active, + element alternate.active { + background-color: transparent; + text-color: @accent; + } + + element selected.active { + background-color: @bg-select; + text-color: @accent; + } + + element normal.urgent, + element selected.urgent { + background-color: @bg-select; + text-color: @clr-error; + } + + element-icon { + size: 20px; + vertical-align: 0.5; + } + + element-text { + text-color: inherit; + expand: true; + highlight: bold ${r.accent}; + } + + /* ── Scrollbar (hidden but defined) ──────────────────────────────────── */ + scrollbar { + background-color: @bg-alt; + handle-color: @fg-dim; + handle-width: 4px; + border-radius: 2px; + width: 6px; + } + ''; + + # ── Rofi config (settings + theme import) ─────────────────────────────── + rofiConfig = '' + /** + * Rofi configuration — managed by Nix (launcher.nix) + * Theme colors come from @import "theme.rasi" + */ + configuration { + modi: "drun,run"; + show-icons: true; + display-drun: " Apps"; + display-run: " Run"; + drun-display-format: "{name}"; + hover-select: false; + /* Accept entry on primary click */ + me-select-entry: ""; + me-accept-entry: "MousePrimary"; + } + + @import "theme.rasi" + ''; + in + { home.packages = [ pkgs.rofi ]; - - # Both files are fully Nix-managed — no dotfiles dir symlink needed. - # Switching themes only requires changing theme.name in the host config. xdg.configFile."rofi/theme.rasi".text = rofiTheme; xdg.configFile."rofi/config.rasi".text = rofiConfig; }; - }; } diff --git a/modules/home/desktop/widgets/network-applet.nix b/modules/home/desktop/widgets/network-applet.nix index beeab26d..fe4f86af 100644 --- a/modules/home/desktop/widgets/network-applet.nix +++ b/modules/home/desktop/widgets/network-applet.nix @@ -1,10 +1,10 @@ # Network manager applet -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Network manager tray applet"; - module = { - nixosSystems.home.packages = [ pkgs.networkmanagerapplet ]; + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.networkmanagerapplet ]; }; } diff --git a/modules/home/desktop/widgets/omarchy-waybar.nix b/modules/home/desktop/widgets/omarchy-waybar.nix index cd6203b8..48269058 100644 --- a/modules/home/desktop/widgets/omarchy-waybar.nix +++ b/modules/home/desktop/widgets/omarchy-waybar.nix @@ -1,93 +1,49 @@ # omarchy-waybar — Omarchy scripts & indicators for Waybar (NixOS port) -# -# Fetches the upstream omarchy repo and makes the bin/ scripts + default/waybar/ -# indicator scripts available at $OMARCHY_PATH so the V3.1b waybar config works -# without a real Omarchy installation. -# -# Scripts required by config.jsonc: -# Bins: omarchy-menu, omarchy-update-available, omarchy-capture-screenrecording, -# omarchy-launch-floating-terminal-with-presentation, -# omarchy-launch-or-focus-tui, omarchy-launch-wifi, omarchy-launch-audio, -# omarchy-launch-bluetooth, omarchy-battery-status, -# omarchy-toggle-idle, omarchy-toggle-notification-silencing, -# omarchy-voxtype-status, omarchy-voxtype-config, omarchy-voxtype-model, -# omarchy-tz-select -# Indicators: $OMARCHY_PATH/default/waybar/indicators/{idle,screen-recording,notification-silencing}.sh -# -{ lib, pkgs, ... }@args: +{ lib, ... }: -let - # ── Fetch omarchy from GitHub ───────────────────────────────────────────── - # Update hash by running: - # nix-prefetch-url --unpack https://github.com/basecamp/omarchy/archive/master.tar.gz - omarchy = pkgs.fetchFromGitHub { - owner = "basecamp"; - repo = "omarchy"; - rev = "master"; - hash = "sha256-tnAFBxdXamEG2qAy4TVUB8dSQrGVUjlQPE42lK4aoiY="; - }; - - # ── Build a derivation that puts all omarchy scripts in $out/bin ────────── - # Copies every omarchy-* script from bin/ and makes them executable. - # No compilation needed — they are plain shell / ruby scripts. - omarchyBins = pkgs.runCommand "omarchy-bins" - { - # Make common runtime tools available on PATH inside the scripts - nativeBuildInputs = [ pkgs.makeWrapper ]; - } - '' - mkdir -p $out/bin - - for f in ${omarchy}/bin/omarchy-*; do - install -m 755 "$f" $out/bin/ - done - ''; - -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Omarchy scripts & waybar indicators (NixOS port)"; - module = { - nixosSystems = { - # ── Runtime packages required by omarchy scripts ──────────────────── - home.packages = with pkgs; [ - omarchyBins # all omarchy-* scripts on $PATH + homeModuleLinux = { pkgs, ... }: + let + omarchy = pkgs.fetchFromGitHub { + owner = "basecamp"; + repo = "omarchy"; + rev = "master"; + hash = "sha256-tnAFBxdXamEG2qAy4TVUB8dSQrGVUjlQPE42lK4aoiY="; + }; - # omarchy-launch-or-focus-tui btop (cpu / memory on-click) + omarchyBins = pkgs.runCommand "omarchy-bins" + { + nativeBuildInputs = [ pkgs.makeWrapper ]; + } + '' + mkdir -p $out/bin + + for f in ${omarchy}/bin/omarchy-*; do + install -m 755 "$f" $out/bin/ + done + ''; + in + { + home.packages = with pkgs; [ + omarchyBins btop - - # omarchy-launch-wifi → nmtui / nm-connection-editor networkmanagerapplet - - # omarchy-launch-audio / omarchy-launch-bluetooth pavucontrol blueman - - # omarchy-capture-screenrecording → wf-recorder + slurp for region wf-recorder slurp - - # omarchy-battery-status (reads upower for time-remaining) upower - - # custom/weather module wttrbar - - # pulseaudio#input/output modules pamixer - pulseaudio # provides pactl - - # xdg-terminal-exec (on-click-right for the omarchy button) + pulseaudio xdg-terminal-exec ]; - # ── Inject OMARCHY_PATH into the user session ──────────────────────── - # Waybar indicator execs reference $OMARCHY_PATH/default/waybar/indicators/*.sh - # home.sessionVariables writes to ~/.profile / PAM env so waybar picks it up. home.sessionVariables = { OMARCHY_PATH = "${omarchy}"; }; }; - }; } diff --git a/modules/home/desktop/widgets/tailscale-systray.nix b/modules/home/desktop/widgets/tailscale-systray.nix new file mode 100644 index 00000000..283070c7 --- /dev/null +++ b/modules/home/desktop/widgets/tailscale-systray.nix @@ -0,0 +1,10 @@ +# Tailscale system tray indicator +{ lib, ... }: + +lib.my.mkFeature { + description = "Tailscale system tray indicator"; + platforms = [ "linux" ]; + homeModuleLinux = { pkgs, osConfig, lib, ... }: { + home.packages = lib.optional (osConfig.services.tailscale.enable or false) pkgs.tailscale-systray; + }; +} diff --git a/modules/home/desktop/widgets/waybar.nix b/modules/home/desktop/widgets/waybar.nix index acc744a4..e6982d16 100644 --- a/modules/home/desktop/widgets/waybar.nix +++ b/modules/home/desktop/widgets/waybar.nix @@ -1,67 +1,59 @@ # Waybar - Wayland status bar -{ lib, config, pkgs, ... }@args: +{ lib, ... }: -let - theme = lib.my.getTheme config; - r = theme.roles; - - # ── Generated GTK CSS theme file ────────────────────────────────────────── - # Written to ~/.config/waybar-theme.css (adjacent to the ~/.config/waybar - # symlink) to avoid the dir-level symlink conflict — same pattern as hypr-theme. - # style.css imports it with: @import "../waybar-theme.css"; - waybartheme = '' - /** - * Generated by Nix — do not edit. - * Theme: ${theme.displayName} - * Variant: ${theme.variant} - * Change by setting theme.name in your host config. - */ - - /* ── Core background / foreground ────────────────────────────────────── */ - @define-color background ${r.background}; - @define-color foreground ${r.foreground}; - - /* ── Aliases used throughout style.css ───────────────────────────────── */ - @define-color bg ${r.background}; - @define-color fg ${r.foreground}; - - /* ── Accent (replaces the hardcoded #df6124 orange) ─────────────────── */ - @define-color red ${r.accent}; - - /* ── Surface / muted tones ───────────────────────────────────────────── */ - @define-color grey ${r.foregroundMuted}; - @define-color grey2 ${r.backgroundFloat}; - - /* ── Semantic roles ──────────────────────────────────────────────────── */ - @define-color surface ${r.backgroundAlt}; - @define-color overlay ${r.backgroundFloat}; - @define-color border ${r.border}; - @define-color accent ${r.accent}; - @define-color accent-alt ${r.accentAlt}; - @define-color clr-error ${r.error}; - @define-color clr-warning ${r.warning}; - @define-color clr-success ${r.success}; - ''; -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Waybar status bar for Wayland"; - module = { - nixosSystems = { + homeModuleLinux = { pkgs, osConfig, lib, ... }: + let + theme = lib.my.getTheme osConfig; + r = theme.roles; + + waybartheme = '' + /** + * Generated by Nix — do not edit. + * Theme: ${theme.displayName} + * Variant: ${theme.variant} + * Change by setting theme.name in your host config. + */ + + /* ── Core background / foreground ────────────────────────────────────── */ + @define-color background ${r.background}; + @define-color foreground ${r.foreground}; + + /* ── Aliases used throughout style.css ───────────────────────────────── */ + @define-color bg ${r.background}; + @define-color fg ${r.foreground}; + + /* ── Accent (replaces the hardcoded #df6124 orange) ─────────────────── */ + @define-color red ${r.accent}; + + /* ── Surface / muted tones ───────────────────────────────────────────── */ + @define-color grey ${r.foregroundMuted}; + @define-color grey2 ${r.backgroundFloat}; + + /* ── Semantic roles ──────────────────────────────────────────────────── */ + @define-color surface ${r.backgroundAlt}; + @define-color overlay ${r.backgroundFloat}; + @define-color border ${r.border}; + @define-color accent ${r.accent}; + @define-color accent-alt ${r.accentAlt}; + @define-color clr-error ${r.error}; + @define-color clr-warning ${r.warning}; + @define-color clr-success ${r.success}; + ''; + in + { home.packages = with pkgs; [ waybar - # Dependencies for waybar modules - pamixer # pulseaudio volume control - brightnessctl # backlight - pavucontrol # pulseaudio GUI (on-click) + pamixer + brightnessctl + pavucontrol ]; - # Generated theme CSS — written adjacent to the waybar dotfiles symlink - # so @import "../waybar-theme.css" resolves correctly from style.css. xdg.configFile."waybar-theme.css".text = waybartheme; }; - }; dotfiles = { path = "waybar"; diff --git a/modules/home/downloads/qbittorrent.nix b/modules/home/downloads/qbittorrent.nix index 5ce06a7e..aff7a7c8 100644 --- a/modules/home/downloads/qbittorrent.nix +++ b/modules/home/downloads/qbittorrent.nix @@ -1,10 +1,9 @@ # qBittorrent torrent client -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "qBittorrent torrent client"; - module = { - allSystems.home.packages = [ pkgs.qbittorrent ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.qbittorrent ]; }; } diff --git a/modules/home/editors/antigravity/antigravity.nix b/modules/home/editors/antigravity/antigravity.nix index 79a8cdf0..0e85e304 100644 --- a/modules/home/editors/antigravity/antigravity.nix +++ b/modules/home/editors/antigravity/antigravity.nix @@ -1,60 +1,7 @@ # Antigravity IDE & standalone Antigravity 2.0 -# Tracks latest via the upstream antigravity-nix flake (auto-updates 3x/week). -# -# On NixOS without a keyring daemon (typical on bare Hyprland), Electron's default -# credential backend silently fails — auth tokens are never saved, so every launch -# looks like a fresh install: no login, no conversation history. -# --password-store=basic tells Electron to store credentials as plain text inside -# the app's --user-data-dir (~/.antigravity-ide), which persists normally. -# -# Config management: when enabled, also manages ~/.gemini/config/AGENTS.md -# and ~/.gemini/antigravity/mcp_config.json (global MCP servers). -{ lib, pkgs, config, ... }@args: +{ lib, ... }: -let - # Wrap Linux binaries to force plain-text credential storage. - ideLinux = pkgs.symlinkJoin { - name = "google-antigravity-ide-with-basic-store"; - paths = [ pkgs.google-antigravity-ide ]; - buildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/antigravity-ide \ - --add-flags "--password-store=basic" \ - --add-flags "--force-color-profile=srgb" - ''; - }; - - antigravityLinux = pkgs.symlinkJoin { - name = "google-antigravity-with-basic-store"; - paths = [ pkgs.google-antigravity ]; - buildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/antigravity \ - --add-flags "--password-store=basic" \ - --add-flags "--force-color-profile=srgb" - ''; - }; - - antigravityWrapper = pkgs.writeShellScriptBin "antigravity" '' - exec open -a "${pkgs.google-antigravity}/Applications/Antigravity.app" --args --password-store=basic "$@" - ''; - - antigravityIdeWrapper = pkgs.writeShellScriptBin "antigravity-ide" '' - exec open -a "${pkgs.google-antigravity-ide}/Applications/Antigravity IDE.app" --args --password-store=basic "$@" - ''; - - - # cfg read via config.modules path-walking so it's available at the top-level let - # (for hasRules/hasMcp guards used in allSystems.home.file below). - modulePath = args._modulePath; - pathParts = lib.splitString "." modulePath; - cfg = lib.foldl' (acc: part: acc.${part} or { }) config.modules pathParts; - - hasRules = (cfg.rules or "") != ""; - hasMcp = (cfg.mcpServers or { }) != { }; - -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Antigravity IDE & standalone Antigravity 2.0"; extraOptions = { @@ -89,38 +36,80 @@ lib.my.mkModuleV2 args { }; }; - # module without cfg parameter — avoids the recursion entirely. - # Package installation doesn't depend on cfg values. - module = { - nixosSystems.home.packages = [ - ideLinux - antigravityLinux - pkgs.google-antigravity-cli - pkgs.mcp-nixos # nixos MCP server — called directly (avoids slow nix run github:...) - ]; + homeModule = { pkgs, lib, featureCfg, ... }: + let + hasRules = (featureCfg.rules or "") != ""; + hasMcp = (featureCfg.mcpServers or { }) != { }; + in + { + programs.antigravity-cli = { + enable = true; + }; - darwinSystems.home.packages = [ - antigravityWrapper - antigravityIdeWrapper - pkgs.google-antigravity-ide - pkgs.google-antigravity - pkgs.google-antigravity-cli - pkgs.mcp-nixos - ]; + home.file = lib.mkMerge [ + (lib.mkIf hasRules { + ".gemini/config/AGENTS.md".text = featureCfg.rules; + }) + (lib.mkIf hasMcp { + ".gemini/antigravity/mcp_config.json".text = + builtins.toJSON { inherit (featureCfg) mcpServers; }; + ".gemini/config/mcp_config.json".text = + builtins.toJSON { inherit (featureCfg) mcpServers; }; + }) + ]; + }; + + homeModuleLinux = { pkgs, ... }: + let + ideLinux = pkgs.symlinkJoin { + name = "google-antigravity-ide-with-basic-store"; + paths = [ pkgs.google-antigravity-ide ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/antigravity-ide \ + --add-flags "--password-store=basic" \ + --add-flags "--force-color-profile=srgb" + ''; + }; - allSystems.programs.antigravity-cli = { - enable = true; + antigravityLinux = pkgs.symlinkJoin { + name = "google-antigravity-with-basic-store"; + paths = [ pkgs.google-antigravity ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/antigravity \ + --add-flags "--password-store=basic" \ + --add-flags "--force-color-profile=srgb" + ''; + }; + in + { + home.packages = [ + ideLinux + antigravityLinux + pkgs.google-antigravity-cli + pkgs.mcp-nixos + ]; }; - # Config files — routed to all enabled users automatically via home.* auto-routing - allSystems.home.file = lib.mkMerge [ - (lib.mkIf hasRules { ".gemini/config/AGENTS.md".text = cfg.rules; }) - (lib.mkIf hasMcp { - ".gemini/antigravity/mcp_config.json".text = - builtins.toJSON { inherit (cfg) mcpServers; }; - ".gemini/config/mcp_config.json".text = - builtins.toJSON { inherit (cfg) mcpServers; }; - }) - ]; - }; + homeModuleDarwin = { pkgs, ... }: + let + antigravityWrapper = pkgs.writeShellScriptBin "antigravity" '' + exec open -a "${pkgs.google-antigravity}/Applications/Antigravity.app" --args --password-store=basic "$@" + ''; + + antigravityIdeWrapper = pkgs.writeShellScriptBin "antigravity-ide" '' + exec open -a "${pkgs.google-antigravity-ide}/Applications/Antigravity IDE.app" --args --password-store=basic "$@" + ''; + in + { + home.packages = [ + antigravityWrapper + antigravityIdeWrapper + pkgs.google-antigravity-ide + pkgs.google-antigravity + pkgs.google-antigravity-cli + pkgs.mcp-nixos + ]; + }; } diff --git a/modules/home/editors/cursor/cursor.nix b/modules/home/editors/cursor/cursor.nix index 15561dc1..258fc01d 100644 --- a/modules/home/editors/cursor/cursor.nix +++ b/modules/home/editors/cursor/cursor.nix @@ -1,13 +1,10 @@ # Cursor IDE - AI-powered code editor -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Cursor IDE - AI-powered code editor"; - module = { - allSystems = { - programs.cursor.enable = true; - home.packages = [ pkgs.cursor-cli ]; - }; + homeModule = { pkgs, ... }: { + programs.cursor.enable = true; + home.packages = [ pkgs.cursor-cli ]; }; } diff --git a/modules/home/editors/ghidra/ghidra.nix b/modules/home/editors/ghidra/ghidra.nix index eacf1525..78f18850 100644 --- a/modules/home/editors/ghidra/ghidra.nix +++ b/modules/home/editors/ghidra/ghidra.nix @@ -1,40 +1,24 @@ # Ghidra + GhidraMCP bridge -# ghidra: NSA reverse engineering suite (12.1.2 in nixpkgs) -# ghidra-mcp: MCP bridge that exposes 256 Ghidra tools to AI clients via HTTP -# -# Topology: -# nix-oldstar — Ghidra runs (GUI or headless), bridge listens on :8089 -# nix-goldstar — bridge binary in PATH for Antigravity MCP config -# -# To start Ghidra headlessly (managed manually or via systemd unit): -# ghidra --headless -import -{ lib, pkgs, config, ... }@args: +{ lib, ... }: -let - # Ghidra writes its config under a version+variant-specific subdirectory. - # NixOS uses the "_NIX" variant suffix. - ghidraConfigDir = ".config/ghidra/ghidra_12.1.2_NIX"; - ghidraPkg = if pkgs ? ghidra-aeon then pkgs.ghidra.withExtensions (exts: [ pkgs.ghidra-aeon ]) else pkgs.ghidra; -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Ghidra reverse-engineering suite + GhidraMCP bridge"; platforms = [ "linux" ]; - module = { - nixosSystems.home = { - packages = [ - ghidraPkg # Ghidra 12.1.2 (with AEON R2 if available) - # ghidra-mcp is a custom overlay package (Linux-only); guard against cross-system - # evaluation where the overlay may not be applied (e.g., Darwin flake check). + homeModuleLinux = { pkgs, lib, ... }: + let + ghidraConfigDir = ".config/ghidra/ghidra_12.1.2_NIX"; + ghidraPkg = if pkgs ? ghidra-aeon then pkgs.ghidra.withExtensions (exts: [ pkgs.ghidra-aeon ]) else pkgs.ghidra; + in + { + home.packages = [ + ghidraPkg ] ++ lib.optional (pkgs ? ghidra-mcp) pkgs.ghidra-mcp ++ lib.optional (pkgs ? maven) pkgs.maven; - # Deploy Java extension into Ghidra's user Extensions directory. - # Ghidra scans this dir on startup and loads GhidraMCP automatically. - file."${ghidraConfigDir}/Extensions/GhidraMCP" = lib.mkIf (pkgs ? ghidra-mcp-extension) { + home.file."${ghidraConfigDir}/Extensions/GhidraMCP" = lib.mkIf (pkgs ? ghidra-mcp-extension) { source = pkgs.ghidra-mcp-extension; recursive = true; }; }; - }; } diff --git a/modules/home/editors/neovim/neovim.nix b/modules/home/editors/neovim/neovim.nix index 48223758..eab4baa7 100644 --- a/modules/home/editors/neovim/neovim.nix +++ b/modules/home/editors/neovim/neovim.nix @@ -1,19 +1,19 @@ # Neovim editor backed by github:GDR/nvim.nix Flake -{ lib, pkgs, inputs, ... }@args: +{ lib, inputs, ... }: -let - wrappedNeovim = inputs.nvim-nix.packages.${pkgs.stdenv.hostPlatform.system}.default; -in -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Neovim editor with Lua-first configuration"; - module = { - # The wrapped neovim has plugins + LSPs on its PATH. - # ripgrep/fzf/fd also added globally for shell usage. - allSystems.home.packages = [ wrappedNeovim ] ++ (with pkgs; [ ripgrep fzf fd ]); - allSystems.xdg.configFile."nvim".source = "${inputs.nvim-nix}/dotfiles/nvim"; - # wl-clipboard is the Wayland clipboard provider; pbcopy/pbpaste are built-in on Darwin - nixosSystems.home.packages = [ pkgs.wl-clipboard ]; + homeModule = { pkgs, ... }: + let + wrappedNeovim = inputs.nvim-nix.packages.${pkgs.stdenv.hostPlatform.system}.default; + in + { + home.packages = [ wrappedNeovim ] ++ (with pkgs; [ ripgrep fzf fd ]); + xdg.configFile."nvim".source = "${inputs.nvim-nix}/dotfiles/nvim"; + }; + + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.wl-clipboard ]; }; } diff --git a/modules/home/editors/vscode-server.nix b/modules/home/editors/vscode-server.nix index 9ad9f5c5..99fe7cf3 100644 --- a/modules/home/editors/vscode-server.nix +++ b/modules/home/editors/vscode-server.nix @@ -1,49 +1,25 @@ # VS Code Server — remote development daemon for VS Code / Cursor / Antigravity IDE -# -# This is a HOME module: enable per-user like any other editor. -# When any enabled user has it, the system daemon activates automatically (via systemModule). -# -# The daemon patches ~/.vscode-server (and sibling dirs) so that the remote server -# binaries VS Code drops there survive NixOS's non-FHS filesystem layout. -# Also enables nix-ld so dynamically-linked server extensions work without friction. -# -# Enable: hostUsers..modules.home.editors.vscode-server.enable = true -# Or via the developer/server profile. -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "VS Code Server / Antigravity IDE remote development daemon"; - # System-level: the daemon and nix-ld are machine-wide concerns. - # Activates when ANY enabled user has this module enabled. - systemModule = { - nixosSystems = { - services.vscode-server = { - enable = true; + nixosModule = { pkgs, ... }: { + services.vscode-server = { + enable = true; - # Watch all remote-server install paths: - # ~/.vscode-server – VS Code / Cursor - # ~/.antigravity-ide-server – Antigravity IDE (current) - # ~/.antigravity-server – Antigravity IDE (legacy) - installPath = [ - "$HOME/.vscode-server" - "$HOME/.antigravity-ide-server" - "$HOME/.antigravity-server" - ]; + installPath = [ + "$HOME/.vscode-server" + "$HOME/.antigravity-ide-server" + "$HOME/.antigravity-server" + ]; - # The Antigravity server startup script calls /usr/bin/pgrep (hardcoded). - extraRuntimeDependencies = [ pkgs.procps ]; - }; + extraRuntimeDependencies = [ pkgs.procps ]; + }; - # Required for VS Code server extensions to work on NixOS (no FHS). - programs.nix-ld.enable = true; + programs.nix-ld.enable = true; - # pgrep must be on PATH for all sessions (/usr/bin is read-only on NixOS). - environment.systemPackages = [ pkgs.procps ]; - }; + environment.systemPackages = [ pkgs.procps ]; }; - - # No home-manager config needed — the daemon does everything at the system level. - module = { }; } diff --git a/modules/home/games/lutris.nix b/modules/home/games/lutris.nix index 541f1326..72317362 100644 --- a/modules/home/games/lutris.nix +++ b/modules/home/games/lutris.nix @@ -1,30 +1,25 @@ # Lutris gaming platform - Linux only -{ lib, pkgs, ... }@args: with lib; +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Lutris gaming platform"; - module = cfg: { - nixosSystems = { - programs.lutris.enable = true; - programs.lutris = { - extraPackages = with pkgs; [ - gamemode - ]; - }; + homeModuleLinux = { pkgs, ... }: { + programs.lutris.enable = true; + programs.lutris.extraPackages = with pkgs; [ + gamemode + ]; - # XDG portal for file dialogs, OpenURI (OAuth callbacks), etc. - # Same pattern as hyprland: gtk fallback since 1.17.0 requires explicit portals.conf - xdg.portal = { - enable = true; - extraPortals = with pkgs; [ xdg-desktop-portal-gtk ]; - config.common.default = "gtk"; - }; - - home.packages = with pkgs; [ - wine - ]; + # XDG portal for file dialogs, OpenURI (OAuth callbacks), etc. + xdg.portal = { + enable = true; + extraPortals = with pkgs; [ xdg-desktop-portal-gtk ]; + config.common.default = "gtk"; }; + + home.packages = with pkgs; [ + wine + ]; }; } diff --git a/modules/home/games/soundpad.nix b/modules/home/games/soundpad.nix index ee8b8471..a8f70fbc 100644 --- a/modules/home/games/soundpad.nix +++ b/modules/home/games/soundpad.nix @@ -1,173 +1,161 @@ # Soundpad — Virtual mic soundboard for Dota 2 -# -# Creates a PipeWire virtual sink/source pair so sound effects play -# through a virtual microphone that Dota 2 teammates hear in voice chat. -# -# Quick start: -# 1. Deploy and restart PipeWire (or reboot) -# 2. Drop sounds as 1.wav, 2.wav, ... 12.wav in ~/.config/soundpad/sounds/ -# 3. Set "Soundpad Mic" as input device in Dota 2 / Discord -# 4. Press F1–F12 (Fn+F1–F12 on Keychron) to play sounds -# 5. Optional: run `soundpad setup` to also pass your real mic through -{ lib, pkgs, ... }@args: - -let - soundpad = pkgs.writeShellScriptBin "soundpad" '' - SOUNDS_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/soundpad/sounds" - RUNTIME_DIR="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" - PID_FILE="$RUNTIME_DIR/soundpad-play.pid" - LOOPBACK_FILE="$RUNTIME_DIR/soundpad-loopback.id" - - mkdir -p "$SOUNDS_DIR" - - # Kill any currently playing sound - kill_current() { - if [ -f "$PID_FILE" ]; then - local pid - pid=$(cat "$PID_FILE" 2>/dev/null || true) - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then - # Kill children (paplay) first, then the subshell - ${pkgs.procps}/bin/pkill -P "$pid" 2>/dev/null || true - kill "$pid" 2>/dev/null || true - fi - rm -f "$PID_FILE" - fi - # Kill any lingering speaker playback - ${pkgs.procps}/bin/pkill -f "paplay.*--volume=" 2>/dev/null || true - } - - case "''${1:-help}" in - setup) - # Start mic passthrough: real mic → soundpad sink → soundpad mic - if [ -f "$LOOPBACK_FILE" ]; then - echo "Mic passthrough already active (module $(cat "$LOOPBACK_FILE"))." - exit 0 - fi - default_source=$(${pkgs.pulseaudio}/bin/pactl get-default-source) - module_id=$(${pkgs.pulseaudio}/bin/pactl load-module module-loopback \ - source="$default_source" sink=soundpad_sink latency_msec=30) - echo "$module_id" > "$LOOPBACK_FILE" - echo "Mic passthrough started (module $module_id)." - echo "Voice + sounds now go through 'Soundpad Mic'." - ;; - - teardown) - # Stop mic passthrough - if [ -f "$LOOPBACK_FILE" ]; then - ${pkgs.pulseaudio}/bin/pactl unload-module "$(cat "$LOOPBACK_FILE")" 2>/dev/null || true - rm -f "$LOOPBACK_FILE" - echo "Mic passthrough stopped." - else - echo "No mic passthrough running." - fi - ;; - - stop) - kill_current - ;; - - status) - echo "=== Soundpad Status ===" - echo "Sounds: $SOUNDS_DIR" - echo "" - if compgen -G "$SOUNDS_DIR/*" > /dev/null 2>&1; then - echo "Available:" - ls -1 "$SOUNDS_DIR" 2>/dev/null | sed 's/^/ /' - else - echo "No sounds found. Add 1.wav, 2.wav, etc. to $SOUNDS_DIR" - fi - echo "" - if [ -f "$LOOPBACK_FILE" ] && ${pkgs.pulseaudio}/bin/pactl list modules short 2>/dev/null | grep -q "$(cat "$LOOPBACK_FILE")"; then - echo "Mic passthrough: active" - else - rm -f "$LOOPBACK_FILE" 2>/dev/null || true - echo "Mic passthrough: off (run 'soundpad setup')" - fi - ;; - - [0-9]|[0-9][0-9]) - # Play sound N through the virtual sink - # Supports variants: 1.wav, 1_a.wav, 1_2.wav — picks randomly - sound_num="$1" - candidates=() - for ext in wav mp3 ogg flac opus; do - # Exact match: N.ext - [ -f "$SOUNDS_DIR/''${sound_num}.''${ext}" ] && \ - candidates+=("$SOUNDS_DIR/''${sound_num}.''${ext}") - # Variants: N_*.ext - for f in "$SOUNDS_DIR/''${sound_num}_"*".''${ext}"; do - [ -f "$f" ] && candidates+=("$f") - done - done - - # No matching file — silent exit - [ ''${#candidates[@]} -eq 0 ] && exit 0 - - # Avoid repeating the same sound twice in a row - LAST_FILE="$RUNTIME_DIR/soundpad-last-$sound_num" - last_played=$(cat "$LAST_FILE" 2>/dev/null || true) - if [ ''${#candidates[@]} -gt 1 ] && [ -n "$last_played" ]; then - # Remove last played from candidates - filtered=() - for c in "''${candidates[@]}"; do - [ "$c" != "$last_played" ] && filtered+=("$c") - done - candidates=("''${filtered[@]}") - fi - - # Pick a random file - sound_file="''${candidates[$((RANDOM % ''${#candidates[@]}))]}" - echo "$sound_file" > "$LAST_FILE" - - kill_current - - # Hold push-to-talk (G=keycode 34), play sound, release G - ( - ${pkgs.ydotool}/bin/ydotool key 34:1 - ${pkgs.pulseaudio}/bin/paplay --volume=81920 --device=soundpad_sink "$sound_file" - ${pkgs.ydotool}/bin/ydotool key 34:0 - ) & - echo $! > "$PID_FILE" - # Also play to speakers at 50% (you hear this) - ${pkgs.pulseaudio}/bin/paplay --volume=32768 "$sound_file" & - disown - ;; - - help|--help|-h) - echo "soundpad — virtual mic soundboard" - echo "" - echo "Usage:" - echo " soundpad Play sound N (matches N.wav, N.mp3, etc.)" - echo " soundpad stop Stop current sound" - echo " soundpad setup Enable mic passthrough (voice + sounds)" - echo " soundpad teardown Disable mic passthrough" - echo " soundpad status Show status and available sounds" - echo "" - echo "Sounds dir: $SOUNDS_DIR" - ;; - - *) - echo "Unknown: $1 (try 'soundpad help')" >&2 - exit 1 - ;; - esac - ''; - -in -lib.my.mkModuleV2 args { +{ lib, ... }: + +lib.my.mkFeature { platforms = [ "linux" ]; description = "Virtual mic soundboard for Dota 2"; requires = [ "home.desktop.services.audio" ]; - module = { - nixosSystems = { + homeModuleLinux = { pkgs, ... }: + let + soundpad = pkgs.writeShellScriptBin "soundpad" '' + SOUNDS_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/soundpad/sounds" + RUNTIME_DIR="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + PID_FILE="$RUNTIME_DIR/soundpad-play.pid" + LOOPBACK_FILE="$RUNTIME_DIR/soundpad-loopback.id" + + mkdir -p "$SOUNDS_DIR" + + # Kill any currently playing sound + kill_current() { + if [ -f "$PID_FILE" ]; then + local pid + pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + # Kill children (paplay) first, then the subshell + ${pkgs.procps}/bin/pkill -P "$pid" 2>/dev/null || true + kill "$pid" 2>/dev/null || true + fi + rm -f "$PID_FILE" + fi + # Kill any lingering speaker playback + ${pkgs.procps}/bin/pkill -f "paplay.*--volume=" 2>/dev/null || true + } + + case "''${1:-help}" in + setup) + # Start mic passthrough: real mic → soundpad sink → soundpad mic + if [ -f "$LOOPBACK_FILE" ]; then + echo "Mic passthrough already active (module $(cat "$LOOPBACK_FILE"))." + exit 0 + fi + default_source=$(${pkgs.pulseaudio}/bin/pactl get-default-source) + module_id=$(${pkgs.pulseaudio}/bin/pactl load-module module-loopback \ + source="$default_source" sink=soundpad_sink latency_msec=30) + echo "$module_id" > "$LOOPBACK_FILE" + echo "Mic passthrough started (module $module_id)." + echo "Voice + sounds now go through 'Soundpad Mic'." + ;; + + teardown) + # Stop mic passthrough + if [ -f "$LOOPBACK_FILE" ]; then + ${pkgs.pulseaudio}/bin/pactl unload-module "$(cat "$LOOPBACK_FILE")" 2>/dev/null || true + rm -f "$LOOPBACK_FILE" + echo "Mic passthrough stopped." + else + echo "No mic passthrough running." + fi + ;; + + stop) + kill_current + ;; + + status) + echo "=== Soundpad Status ===" + echo "Sounds: $SOUNDS_DIR" + echo "" + if compgen -G "$SOUNDS_DIR/*" > /dev/null 2>&1; then + echo "Available:" + ls -1 "$SOUNDS_DIR" 2>/dev/null | sed 's/^/ /' + else + echo "No sounds found. Add 1.wav, 2.wav, etc. to $SOUNDS_DIR" + fi + echo "" + if [ -f "$LOOPBACK_FILE" ] && ${pkgs.pulseaudio}/bin/pactl list modules short 2>/dev/null | grep -q "$(cat "$LOOPBACK_FILE")"; then + echo "Mic passthrough: active" + else + rm -f "$LOOPBACK_FILE" 2>/dev/null || true + echo "Mic passthrough: off (run 'soundpad setup')" + fi + ;; + + [0-9]|[0-9][0-9]) + # Play sound N through the virtual sink + # Supports variants: 1.wav, 1_a.wav, 1_2.wav — picks randomly + sound_num="$1" + candidates=() + for ext in wav mp3 ogg flac opus; do + # Exact match: N.ext + [ -f "$SOUNDS_DIR/''${sound_num}.''${ext}" ] && \ + candidates+=("$SOUNDS_DIR/''${sound_num}.''${ext}") + # Variants: N_*.ext + for f in "$SOUNDS_DIR/''${sound_num}_"*".''${ext}"; do + [ -f "$f" ] && candidates+=("$f") + done + done + + # No matching file — silent exit + [ ''${#candidates[@]} -eq 0 ] && exit 0 + + # Avoid repeating the same sound twice in a row + LAST_FILE="$RUNTIME_DIR/soundpad-last-$sound_num" + last_played=$(cat "$LAST_FILE" 2>/dev/null || true) + if [ ''${#candidates[@]} -gt 1 ] && [ -n "$last_played" ]; then + # Remove last played from candidates + filtered=() + for c in "''${candidates[@]}"; do + [ "$c" != "$last_played" ] && filtered+=("$c") + done + candidates=("''${filtered[@]}") + fi + + # Pick a random file + sound_file="''${candidates[$((RANDOM % ''${#candidates[@]}))]}" + echo "$sound_file" > "$LAST_FILE" + + kill_current + + # Hold push-to-talk (G=keycode 34), play sound, release G + ( + ${pkgs.ydotool}/bin/ydotool key 34:1 + ${pkgs.pulseaudio}/bin/paplay --volume=81920 --device=soundpad_sink "$sound_file" + ${pkgs.ydotool}/bin/ydotool key 34:0 + ) & + echo $! > "$PID_FILE" + # Also play to speakers at 50% (you hear this) + ${pkgs.pulseaudio}/bin/paplay --volume=32768 "$sound_file" & + disown + ;; + + help|--help|-h) + echo "soundpad — virtual mic soundboard" + echo "" + echo "Usage:" + echo " soundpad Play sound N (matches N.wav, N.mp3, etc.)" + echo " soundpad stop Stop current sound" + echo " soundpad setup Enable mic passthrough (voice + sounds)" + echo " soundpad teardown Disable mic passthrough" + echo " soundpad status Show status and available sounds" + echo "" + echo "Sounds dir: $SOUNDS_DIR" + ;; + + *) + echo "Unknown: $1 (try 'soundpad help')" >&2 + exit 1 + ;; + esac + ''; + in + { home.packages = [ soundpad pkgs.ydotool ]; # ydotoold daemon — creates a permanent virtual keyboard. - # Starts at login so Dota sees it as an existing device, not a new controller. systemd.user.services.ydotoold = { Unit.Description = "ydotool virtual input daemon"; Service = { @@ -178,8 +166,7 @@ lib.my.mkModuleV2 args { Install.WantedBy = [ "default.target" ]; }; - # PipeWire virtual sink → source loopback (auto-created on PipeWire start). - # "Soundpad Sink" receives played audio, "Soundpad Mic" appears as a mic. + # PipeWire virtual sink → source loopback xdg.configFile."pipewire/pipewire.conf.d/20-soundpad.conf".text = '' context.modules = [ { @@ -203,5 +190,4 @@ lib.my.mkModuleV2 args { ] ''; }; - }; } diff --git a/modules/home/games/steam.nix b/modules/home/games/steam.nix index 1c19b3c2..fd692c0b 100644 --- a/modules/home/games/steam.nix +++ b/modules/home/games/steam.nix @@ -1,43 +1,50 @@ -# Steam gaming platform - Linux only (system-wide when any user enables "games" tag) -{ lib, pkgs, ... }@args: with lib; +# Steam gaming platform - Linux only +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "Steam gaming platform with Gamescope support"; + extraOptions = { - enableGamescope = mkOption { + enableGamescope = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; description = "Whether to enable Gamescope support for Steam"; }; - remotePlayOpenFirewall = mkOption { + remotePlayOpenFirewall = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; description = "Open ports in the firewall for Steam Remote Play"; }; - dedicatedServerOpenFirewall = mkOption { + dedicatedServerOpenFirewall = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; description = "Open ports in the firewall for Source Dedicated Server"; }; - extraCodecs = mkOption { + extraCodecs = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; description = "Add GStreamer plugins (H.265/HEVC, etc.) to Steam FHS for codec playback"; }; }; - systemModule = cfg: { - nixosSystems = { - # Enable the Steam program itself (system-level) + + nixosModule = { pkgs, lib, featureCfg, ... }: + let + enableGamescope = featureCfg.enableGamescope or true; + remotePlayOpenFirewall = featureCfg.remotePlayOpenFirewall or true; + dedicatedServerOpenFirewall = featureCfg.dedicatedServerOpenFirewall or true; + extraCodecs = featureCfg.extraCodecs or true; + in + { programs.steam = { enable = true; - remotePlay.openFirewall = cfg.remotePlayOpenFirewall; - dedicatedServer.openFirewall = cfg.dedicatedServerOpenFirewall; - gamescopeSession.enable = cfg.enableGamescope; - extraPackages = with pkgs; (optionals cfg.enableGamescope [ + remotePlay.openFirewall = remotePlayOpenFirewall; + dedicatedServer.openFirewall = dedicatedServerOpenFirewall; + gamescopeSession.enable = enableGamescope; + extraPackages = with pkgs; (lib.optionals enableGamescope [ gamescope libxcursor libxi @@ -48,8 +55,8 @@ lib.my.mkModuleV2 args { libvorbis libkrb5 keyutils - ]) ++ (optionals cfg.extraCodecs [ - gst_all_1.gst-plugins-bad # H.265/HEVC + ]) ++ (lib.optionals extraCodecs [ + gst_all_1.gst-plugins-bad gst_all_1.gst-plugins-ugly gst_all_1.gst-libav gst_all_1.gstreamer @@ -61,23 +68,18 @@ lib.my.mkModuleV2 args { ]; }; - # Enable Gamescope as a system program - programs.gamescope = mkIf cfg.enableGamescope { + programs.gamescope = lib.mkIf enableGamescope { enable = true; - capSysNice = mkForce false; + capSysNice = lib.mkForce false; }; - # Hardware-specific support hardware.steam-hardware.enable = true; - # Allow unfree packages for Steam nixpkgs.config.allowUnfreePredicate = pkg: - builtins.elem (getName pkg) [ + builtins.elem (lib.getName pkg) [ "steam" "steam-original" "steam-run" ]; }; - }; - module = { }; # Steam is system-level only, no home-manager config } diff --git a/modules/home/media/iina.nix b/modules/home/media/iina.nix index 5835fe00..21452dc8 100644 --- a/modules/home/media/iina.nix +++ b/modules/home/media/iina.nix @@ -1,10 +1,10 @@ # IINA media player for macOS -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "darwin" ]; description = "IINA media player for macOS"; - module = { - darwinSystems.home.packages = [ pkgs.iina ]; + homeModuleDarwin = { pkgs, ... }: { + home.packages = [ pkgs.iina ]; }; } diff --git a/modules/home/media/spotify.nix b/modules/home/media/spotify.nix index f98d146e..d07ddc83 100644 --- a/modules/home/media/spotify.nix +++ b/modules/home/media/spotify.nix @@ -1,10 +1,9 @@ # Spotify music streaming -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Spotify music streaming client"; - module = { - allSystems.home.packages = [ pkgs.spotify ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.spotify ]; }; } diff --git a/modules/home/media/vlc.nix b/modules/home/media/vlc.nix index 8545ff48..2e496658 100644 --- a/modules/home/media/vlc.nix +++ b/modules/home/media/vlc.nix @@ -1,9 +1,10 @@ -{ lib, pkgs, ... }@args: +# VLC media player +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" ]; description = "VLC media player"; - module = { - nixosSystems.home.packages = [ pkgs.vlc ]; + homeModuleLinux = { pkgs, ... }: { + home.packages = [ pkgs.vlc ]; }; } diff --git a/modules/home/messengers/telegram.nix b/modules/home/messengers/telegram.nix index 34ca4766..30608537 100644 --- a/modules/home/messengers/telegram.nix +++ b/modules/home/messengers/telegram.nix @@ -1,10 +1,9 @@ # Telegram messenger -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { - platforms = [ "linux" "darwin" ]; +lib.my.mkFeature { description = "Telegram desktop messenger"; - module = { - allSystems.home.packages = [ pkgs.telegram-desktop ]; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.telegram-desktop ]; }; } diff --git a/modules/home/security/bitwarden.nix b/modules/home/security/bitwarden.nix index fca6f028..0ab7ba30 100644 --- a/modules/home/security/bitwarden.nix +++ b/modules/home/security/bitwarden.nix @@ -1,17 +1,10 @@ # Bitwarden - secure password manager -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Bitwarden password manager"; - module = { - # Darwin: use Homebrew cask to avoid nixpkgs compiler-rt build chain - darwinSystems = { - homebrew.casks = [ "bitwarden" ]; - }; - # Linux: use nixpkgs package - nixosSystems = { - home.packages = [ pkgs.bitwarden-desktop ]; - }; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.bitwarden-desktop ]; }; } diff --git a/modules/home/security/clavis.nix b/modules/home/security/clavis.nix index 0575f4c6..51a4501a 100644 --- a/modules/home/security/clavis.nix +++ b/modules/home/security/clavis.nix @@ -1,7 +1,6 @@ -# Clavis — Native macOS Ed25519 Keychain SSH Agent & age plugin with Touch ID -{ lib, pkgs, ... }@args: +{ lib, pkgs ? null, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Clavis — macOS Keychain SSH Agent & age plugin with Touch ID"; platforms = [ "darwin" ]; @@ -13,18 +12,22 @@ lib.my.mkModuleV2 args { }; package = lib.mkOption { type = lib.types.nullOr lib.types.package; - default = pkgs.clavis; + default = if pkgs != null && pkgs ? clavis then pkgs.clavis else null; description = "Clavis package to install"; }; }; - module = cfg: { - darwinSystems = { - home.packages = lib.optional (cfg.package != null) cfg.package; + homeModuleDarwin = { pkgs, lib, featureCfg, ... }: + let + package = if (featureCfg.package or null) != null then featureCfg.package else pkgs.clavis or null; + in + { + home.packages = lib.optional (package != null) package; - launchd.user.agents.clavis-agent = lib.mkIf (cfg.autostart && cfg.package != null) { - command = "${cfg.package}/bin/clavis-agent --daemon"; - serviceConfig = { + launchd.agents.clavis-agent = lib.mkIf ((featureCfg.autostart or true) && package != null) { + enable = true; + config = { + ProgramArguments = [ "${package}/bin/clavis-agent" "--daemon" ]; Label = "com.clavis.agent"; RunAtLoad = true; KeepAlive = false; @@ -32,5 +35,4 @@ lib.my.mkModuleV2 args { }; }; }; - }; } diff --git a/modules/home/security/keepassxc.nix b/modules/home/security/keepassxc.nix index e0d2898d..573fd6ce 100644 --- a/modules/home/security/keepassxc.nix +++ b/modules/home/security/keepassxc.nix @@ -1,11 +1,9 @@ # KeePassXC password manager -{ lib, pkgs, ... }@args: +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "KeePassXC password manager"; - module = { - allSystems = { - home.packages = [ pkgs.keepassxc ]; - }; + homeModule = { pkgs, ... }: { + home.packages = [ pkgs.keepassxc ]; }; } diff --git a/modules/home/shell/git.nix b/modules/home/shell/git.nix new file mode 100644 index 00000000..44bfe7a2 --- /dev/null +++ b/modules/home/shell/git.nix @@ -0,0 +1,86 @@ +# Git configuration - reads user info and signing key from hostUsers +{ lib, ... }: + +lib.my.mkFeature { + description = "Git configuration (reads from hostUsers)"; + platforms = [ "linux" "darwin" ]; + + extraOptions = { + editor = lib.mkOption { + type = lib.types.str; + default = "nvim"; + description = "Default Git editor"; + }; + }; + + homeModule = { config, osConfig, pkgs, lib, featureCfg, userName, ... }: + let + isDarwin = pkgs.stdenv.hostPlatform.isDarwin; + homeDir = if isDarwin then "/Users" else "/home"; + userCfg = osConfig.hostUsers.${userName} or { }; + + # Build key path from key config (public key for signing) + keyPath = key: + let + keyExt = { + "rsa" = "id_rsa"; + "ed25519" = "id_ed25519"; + "ecdsa" = "id_ecdsa"; + "ecdsa-sk" = "id_ecdsa_sk"; + }.${key.type}; + in + "${homeDir}/${userName}/.ssh/${key.name}_${keyExt}.pub"; + + # Get default git signing key for a user (key with "git" in purpose) + getGitKey = user: + let + keys = user.keys or [ ]; + gitKeys = lib.filter (k: lib.elem "git" (k.purpose or [ ])) keys; + defaultGitKey = lib.findFirst (k: k.isDefault or false) null gitKeys; + in + if defaultGitKey != null then defaultGitKey + else if gitKeys != [ ] then lib.head gitKeys + else null; + + gitKey = getGitKey userCfg; + signingKeyPath = if gitKey != null then keyPath gitKey else null; + + clavisModuleEnabled = lib.my.isFeatureEnabledForUser { + config = osConfig; + inherit userName; + modulePath = "home.security.clavis"; + }; + clavisEnabled = isDarwin && ((userCfg.clavisAgent.enable or false) || clavisModuleEnabled); + bwEnabled = userCfg.bitwardenSshAgent.enable or false; + hasAgent = clavisEnabled || bwEnabled; + + # Dynamic signing via SSH agent when no static key is specified + useDynamicSigning = signingKeyPath == null && hasAgent; + in + { + programs.git = { + enable = true; + + signing = { + key = if signingKeyPath != null then signingKeyPath else null; + signByDefault = signingKeyPath != null || useDynamicSigning; + }; + + settings = { + user = { + name = userCfg.fullName; + email = userCfg.email; + }; + core.editor = featureCfg.editor or "nvim"; + push.autoSetupRemote = true; + pull.rebase = true; + init.defaultBranch = "main"; + gpg = { + format = "ssh"; + } // lib.optionalAttrs useDynamicSigning { + ssh.defaultKeyCommand = "sh -c 'echo key::$(ssh-add -L | head -1)'"; + }; + }; + }; + }; +} diff --git a/modules/home/shell/ssh/ssh.nix b/modules/home/shell/ssh/ssh.nix index 0e76fd61..6029eacd 100644 --- a/modules/home/shell/ssh/ssh.nix +++ b/modules/home/shell/ssh/ssh.nix @@ -1,16 +1,11 @@ # SSH client configuration - reads keys and config from hostUsers # User-level module (per-user SSH config) -{ lib, config, pkgs, ... }@args: +{ lib, ... }: let - # Get enabled users - enabledUsers = lib.filterAttrs (_: u: u.enable) (config.hostUsers or { }); - # Convert SSH array to matchBlocks attrs format sshArrayToMatchBlocks = userName: sshArray: let - # Convert a single SSH host entry to a settings block - # Uses upstream OpenSSH directive names as required by programs.ssh.settings sshEntryToSettingsBlock = entry: lib.optionalAttrs (entry.user != null) { @@ -25,17 +20,19 @@ let Hostname = entry.hostname; } // entry.extraOptions; in - # Convert array to attrs: [{ host = "*"; ... }] -> { "*" = { ... }; } lib.listToAttrs (map (entry: { name = entry.host; value = sshEntryToSettingsBlock entry; }) sshArray); +in +lib.my.mkFeature { + description = "SSH client configuration (reads keys and config from hostUsers)"; - # Build SSH config for a user - mkUserSSHConfig = pkgs: userName: userCfg: + homeModule = { config, osConfig, pkgs, lib, userName, ... }: let + userCfg = osConfig.hostUsers.${userName} or { }; sshArray = userCfg.ssh or [ ]; includes = userCfg.sshIncludes or [ "~/.ssh/config.d/*" ]; baseSettings = sshArrayToMatchBlocks userName sshArray; @@ -44,7 +41,11 @@ let homePrefix = if isDarwin then "/Users" else "/home"; # Clavis SSH Agent integration (macOS-only; guard against home.security.enable cascade on Linux) - clavisModuleEnabled = lib.my.shouldEnableModule { inherit config; modulePath = "home.security.clavis"; }; + clavisModuleEnabled = lib.my.isFeatureEnabledForUser { + config = osConfig; + inherit userName; + modulePath = "home.security.clavis"; + }; clavisCfg = userCfg.clavisAgent or { }; clavisEnabled = isDarwin && ((clavisCfg.enable or false) || clavisModuleEnabled); defaultClavisSocket = "${homePrefix}/${userName}/.ssh/clavis.sock"; @@ -54,7 +55,7 @@ let bwCfg = userCfg.bitwardenSshAgent or { }; bwEnabled = bwCfg.enable or false; defaultBwSocket = - if pkgs.stdenv.hostPlatform.isDarwin + if isDarwin then "~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock" else "~/.bitwarden/ssh-agent.sock"; bwSocket = if (bwCfg.socketPath or null != null) then bwCfg.socketPath else defaultBwSocket; @@ -76,8 +77,6 @@ let baseControlSettings = { "*" = { - # ControlMaster = "auto"; - # ControlPath = "~/.ssh/sockets/%r@%h:%p"; IdentitiesOnly = if agentEnabled then "no" else "yes"; AddKeysToAgent = if agentEnabled then "no" else "yes"; } // lib.optionalAttrs (defaultIdentityFile != null) { @@ -88,14 +87,12 @@ let }; }; - # Inject ControlMaster & IdentityAgent into '*' host setting if enabled settings = if agentEnabled then lib.recursiveUpdate (lib.recursiveUpdate baseControlSettings { "*" = { IdentityAgent = agentSocket; }; }) baseSettings else lib.recursiveUpdate baseControlSettings baseSettings; - # Write declared public keys to ~/.ssh/_id_.pub pubKeyFiles = lib.listToAttrs (map (key: { name = ".ssh/${key.name}_id_${key.type}.pub"; @@ -119,23 +116,18 @@ let SSH_AUTH_SOCK = agentSocket; }; - # Ensure SSH_AUTH_SOCK is always set in Zsh even when __HM_SESS_VARS_SOURCED=1 - # is already inherited from a parent GUI app or terminal. programs.zsh.envExtra = lib.mkIf agentEnabled '' if [ -z "''${SSH_CONNECTION-}" ]; then export SSH_AUTH_SOCK="${agentSocket}" fi ''; - # On macOS, propagate SSH_AUTH_SOCK to launchd user environment so GUI apps - # (IDEs, Git clients) don't fall back to Apple's com.openssh.ssh-agent socket. - home.activation.setLaunchdSshAuthSock = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && agentEnabled) ( + home.activation.setLaunchdSshAuthSock = lib.mkIf (isDarwin && agentEnabled) ( lib.hm.dag.entryAfter [ "writeBoundary" ] '' /bin/launchctl setenv SSH_AUTH_SOCK "${agentSocket}" || true '' ); - # Pre-activation: remove regular file ~/.ssh/config so Home Manager can recreate the symlink with fresh config home.activation.removeOldSshConfig = lib.hm.dag.entryBefore [ "checkLinkTargets" ] '' sshConfig="$HOME/.ssh/config" if [ -f "$sshConfig" ] && [ ! -L "$sshConfig" ]; then @@ -143,9 +135,6 @@ let fi ''; - # SSH refuses symlinks to world-readable Nix store files. - # Replace the symlink with a proper copy at 0600 after each switch. - # Also ensure ~/.ssh/sockets exists for ControlMaster multiplexing. home.activation.fixSshConfigPerms = lib.hm.dag.entryAfter [ "writeBoundary" ] '' mkdir -p "$HOME/.ssh/sockets" chmod 700 "$HOME/.ssh/sockets" @@ -158,14 +147,4 @@ let fi ''; }; -in -lib.my.mkModuleV2 args { - description = "SSH client configuration (reads keys and config from hostUsers)"; - - module = _: { - allSystems = { - # Apply config to all enabled users - home-manager.users = lib.mapAttrs (mkUserSSHConfig pkgs) enabledUsers; - }; - }; } diff --git a/modules/home/shell/tmux/tmux.nix b/modules/home/shell/tmux/tmux.nix index 575ce91c..a948a473 100644 --- a/modules/home/shell/tmux/tmux.nix +++ b/modules/home/shell/tmux/tmux.nix @@ -1,14 +1,14 @@ # Tmux terminal multiplexer -{ lib, config, pkgs, ... }@args: +{ lib, ... }: -let - t = (lib.my.getTheme config).roles; -in -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Tmux terminal multiplexer"; - module = { - allSystems = { + homeModule = { pkgs, osConfig, lib, ... }: + let + t = (lib.my.getTheme osConfig).roles; + in + { programs.tmux = { enable = false; shell = "${pkgs.zsh}/bin/zsh"; @@ -54,7 +54,7 @@ lib.my.mkModuleV2 args { # ── Window ──────────────────────────────────────────────────────── bind c new-window -c "#{pane_current_path}" - # ── Status bar (theme: ${(lib.my.getTheme config).displayName}) ── + # ── Status bar (theme: ${(lib.my.getTheme osConfig).displayName}) ── set -g status-style 'bg=${t.background} fg=${t.foreground}' set -g status-left '#[fg=${t.background},bg=${t.accent},bold] #S ' set -g status-right '#[fg=${t.foreground}] %H:%M ' @@ -67,7 +67,4 @@ lib.my.mkModuleV2 args { ''; }; }; - }; - # Note: no dotfiles key — programs.tmux manages ~/.config/tmux/tmux.conf directly. - # Adding a dir-level dotfiles symlink here would conflict with programs.tmux output. } diff --git a/modules/home/shell/zsh/zsh.nix b/modules/home/shell/zsh/zsh.nix index 19c63a12..4f06b070 100644 --- a/modules/home/shell/zsh/zsh.nix +++ b/modules/home/shell/zsh/zsh.nix @@ -1,46 +1,51 @@ # Zsh shell configuration with oh-my-zsh and zplug -{ lib, pkgs, config, system, self, _modulePath, ... }@args: with lib; -let - isDarwin = system == "aarch64-darwin" || system == "x86_64-darwin"; -in -lib.my.mkModuleV2 args { +{ lib, ... }: + +lib.my.mkFeature { platforms = [ "linux" "darwin" ]; description = "Zsh shell configuration with oh-my-zsh and zplug"; + extraOptions = { - showHostname = mkOption { + showHostname = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; description = "Show hostname in the prompt (Powerlevel10k)"; }; }; - systemModule = { - allSystems = { - # System-level config (NixOS programs.zsh, users.users) - # Enable zsh at system level (required for users to use zsh as shell) - programs.zsh.enable = true; - environment.systemPackages = with pkgs; [ eza ]; + nixosModule = { pkgs, lib, selectedUsers, ... }: { + programs.zsh.enable = true; + users.users = lib.genAttrs selectedUsers (_: { + shell = pkgs.zsh; + }); + }; - # Set user's default shell to zsh (only for users who enabled zsh) - users.users = lib.my.mkUsersAttrs { inherit config _modulePath; } (username: { - shell = pkgs.zsh; - }); - }; + darwinModule = { pkgs, lib, selectedUsers, ... }: { + programs.zsh.enable = true; + users.users = lib.genAttrs selectedUsers (_: { + shell = pkgs.zsh; + }); }; - module = cfg: { - allSystems = { + + homeModule = { pkgs, lib, featureCfg, ... }: + let + isDarwin = pkgs.stdenv.hostPlatform.isDarwin; + showHostname = featureCfg.showHostname or true; + in + { + home.packages = with pkgs; [ eza ]; + programs.atuin = { enable = true; enableZshIntegration = true; - flags = [ "--disable-up-arrow" ]; # Keep up-arrow for normal history + flags = [ "--disable-up-arrow" ]; settings = { - auto_sync = false; # No cloud sync + auto_sync = false; search_mode = "fuzzy"; - filter_mode = "global"; # Default to all history - filter_mode_shell_up_key_binding = "directory"; # Up arrow = directory context + filter_mode = "global"; + filter_mode_shell_up_key_binding = "directory"; show_preview = true; style = "compact"; - # In search UI: Ctrl+R cycles filter modes (global/host/session/directory) }; }; @@ -51,25 +56,10 @@ lib.my.mkModuleV2 args { oh-my-zsh.enable = true; - # Disable p10k instant prompt before it loads (via .zshenv, which runs before plugins). - # instant_prompt=verbose hangs in VSCode/Antigravity integrated terminals. envExtra = '' typeset -g POWERLEVEL9K_INSTANT_PROMPT=off ''; - # Guard zplug's check/install against non-interactive shells. - # home-manager emits `if ! zplug check; then zplug install; fi` unconditionally; - # `zplug check` spawns git processes that hang in non-TTY terminals (e.g. Antigravity). - # Since HM manages plugins declaratively, they're always present — skip the check. - # initExtraFirst = '' - # # Make `zplug check` always pass so install is never triggered. - # # Plugins are guaranteed present by home-manager; no git fetch needed. - # zplug() { - # if [[ "$1" == "check" ]]; then return 0; fi - # builtin command zplug "$@" - # } - # ''; - zplug = { enable = true; plugins = [ @@ -96,11 +86,11 @@ lib.my.mkModuleV2 args { } zle -N edit-last-or-current bindkey "\C-e" edit-last-or-current - '' + optionalString (!cfg.showHostname) '' + '' + lib.optionalString (!showHostname) '' # Hide hostname in prompt (set by modules.home.shell.zsh.showHostname = false) typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n' typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%n' - '' + optionalString isDarwin '' + '' + lib.optionalString isDarwin '' if [[ $(uname -m) == 'arm64' ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" fi @@ -115,7 +105,7 @@ lib.my.mkModuleV2 args { }; }; }; - }; + dotfiles = { path = "zsh"; source = "modules/home/shell/zsh/dotfiles"; diff --git a/modules/home/terminal/ghostty/ghostty.nix b/modules/home/terminal/ghostty/ghostty.nix index 88ef8325..1c1768be 100644 --- a/modules/home/terminal/ghostty/ghostty.nix +++ b/modules/home/terminal/ghostty/ghostty.nix @@ -1,11 +1,9 @@ # Ghostty terminal emulator -{ lib, config, pkgs, ... }@args: +{ lib, ... }: let - # Strip leading "#" — Ghostty color values don't use the hash prefix hex = c: lib.removePrefix "#" c; - # Generate a Ghostty theme file from a theme attrset mkGhosttyTheme = theme: let t = theme.terminal; in '' @@ -35,21 +33,28 @@ let selection-foreground = ${hex t.selectionText} split-divider-color = ${hex theme.roles.border} ''; - - theme = lib.my.getTheme config; in -lib.my.mkModuleV2 args { +lib.my.mkFeature { description = "Ghostty terminal emulator"; - module = { - # Generated theme file — written by Nix, sourced by config via config-file = theme - # Works on both NixOS and Darwin (xdg.configFile is cross-platform in home-manager) - allSystems.xdg.configFile."ghostty/theme".text = mkGhosttyTheme theme; - nixosSystems.home.packages = [ pkgs.ghostty ]; - darwinSystems.homebrew.casks = [ "ghostty" ]; - }; + homeModule = { osConfig, pkgs, lib, ... }: + let + theme = lib.my.getTheme osConfig; + in + { + programs.ghostty = { + enable = true; + package = + if pkgs.stdenv.hostPlatform.isDarwin + then pkgs.ghostty-bin + else pkgs.ghostty; + }; + + # Generated theme file — written by Nix, sourced by config via config-file = theme + # Works on both NixOS and Darwin (xdg.configFile is cross-platform in home-manager) + xdg.configFile."ghostty/theme".text = mkGhosttyTheme theme; + }; - # Symlink only the config file so home-manager can also write ghostty/theme dotfiles = { path = "ghostty/config"; source = "modules/home/terminal/ghostty/dotfiles/config"; diff --git a/modules/home/utils/macfuse.nix b/modules/home/utils/macfuse.nix index 5b13268f..e9993579 100644 --- a/modules/home/utils/macfuse.nix +++ b/modules/home/utils/macfuse.nix @@ -1,14 +1,13 @@ # macFUSE - FUSE filesystem support for macOS # Required for mounting NTFS, SSHFS, and other FUSE-based filesystems -{ lib, ... }@args: -lib.my.mkModuleV2 args { +# Note: nixpkgs only provides macfuse-stubs (build-time headers); the macOS kernel extension (.fs) requires Homebrew cask +{ lib, ... }: + +lib.my.mkFeature { platforms = [ "darwin" ]; # macOS only description = "macFUSE filesystem support"; - module = { - darwinSystems = { - homebrew.casks = [ "macfuse" ]; - }; + darwinModule = { + homebrew.casks = [ "macfuse" ]; }; } - diff --git a/modules/home/utils/obsidian.nix b/modules/home/utils/obsidian.nix index 963d61b1..9f3aaaaf 100644 --- a/modules/home/utils/obsidian.nix +++ b/modules/home/utils/obsidian.nix @@ -1,14 +1,13 @@ # Obsidian - Knowledge base and note-taking app -{ lib, pkgs, ... }@args: -lib.my.mkModuleV2 args { +{ lib, ... }: + +lib.my.mkFeature { description = "Obsidian knowledge base and note-taking app"; - module = { - allSystems = { - programs.obsidian = { - enable = true; - cli.enable = true; - }; + homeModule = _: { + programs.obsidian = { + enable = true; + cli.enable = true; }; }; } diff --git a/modules/home/utils/raycast.nix b/modules/home/utils/raycast.nix index 274977d1..ffb753c9 100644 --- a/modules/home/utils/raycast.nix +++ b/modules/home/utils/raycast.nix @@ -1,13 +1,11 @@ # Raycast - Spotlight replacement and productivity launcher for macOS -{ lib, pkgs, ... }@args: -lib.my.mkModuleV2 args { - platforms = [ "darwin" ]; # macOS only +{ lib, ... }: + +lib.my.mkFeature { + platforms = [ "darwin" ]; description = "Raycast launcher"; - module = { - darwinSystems = { - home.packages = [ pkgs.raycast ]; - }; + homeModuleDarwin = { pkgs, ... }: { + home.packages = [ pkgs.raycast ]; }; } - diff --git a/modules/home/virtualisation/docker.nix b/modules/home/virtualisation/docker.nix index ab63e755..59db7827 100644 --- a/modules/home/virtualisation/docker.nix +++ b/modules/home/virtualisation/docker.nix @@ -1,26 +1,23 @@ # Docker container runtime -{ lib, pkgs, config, _modulePath, ... }@args: with lib; +{ lib, ... }: -lib.my.mkModuleV2 args { +lib.my.mkFeature { platforms = [ "linux" "darwin" ]; description = "Docker container runtime"; - systemModule = { - nixosSystems = { - # Linux: Enable docker daemon system-wide, add users to docker group - virtualisation.docker.enable = true; - # Add only users who enabled Docker to docker group - users.users = lib.my.mkUsersAttrs { inherit config _modulePath; } (username: { - extraGroups = [ "docker" ]; - }); - }; - darwinSystems = { - # Darwin: install docker CLI system-wide via native Nix (pkgs.docker supports Darwin) - environment.systemPackages = with pkgs; [ - colima - docker - docker-credential-helpers - ]; - }; + nixosModule = { selectedUsers, lib, ... }: { + virtualisation.docker.enable = true; + + users.users = lib.genAttrs selectedUsers (_: { + extraGroups = [ "docker" ]; + }); + }; + + homeModuleDarwin = { pkgs, ... }: { + home.packages = with pkgs; [ + colima + docker + docker-credential-helpers + ]; }; } diff --git a/modules/home/virtualisation/virtualbox.nix b/modules/home/virtualisation/virtualbox.nix index 81e8003f..9239e257 100644 --- a/modules/home/virtualisation/virtualbox.nix +++ b/modules/home/virtualisation/virtualbox.nix @@ -1,23 +1,28 @@ # VirtualBox virtualization -{ lib, pkgs, config, _modulePath, ... }@args: with lib; -lib.my.mkModuleV2 args { +{ lib, ... }: + +lib.my.mkFeature { platforms = [ "linux" "darwin" ]; description = "VirtualBox virtualization"; - systemModule = { - nixosSystems = { - # Linux: Enable VirtualBox host, add users to vboxusers group - virtualisation.virtualbox.host.enable = true; - # Add only users who enabled VirtualBox to vboxusers group - users.users = lib.my.mkUsersAttrs { inherit config _modulePath; } (username: { - extraGroups = [ "vboxusers" ]; - }); - }; + nixosModule = { selectedUsers, lib, ... }: { + # Linux: Enable VirtualBox host, add users to vboxusers group + virtualisation.virtualbox.host.enable = true; + + # Add only users who enabled VirtualBox to vboxusers group + users.users = lib.genAttrs selectedUsers (_: { + extraGroups = [ "vboxusers" ]; + }); }; - module = { - nixosSystems.home.packages = with pkgs; [ + + # Darwin: pkgs.virtualbox in nixpkgs is x86_64-linux only (requires Oracle signed macOS kext/DriverKit installer) + darwinModule = { + homebrew.casks = [ "virtualbox" ]; + }; + + homeModuleLinux = { pkgs, ... }: { + home.packages = with pkgs; [ virtualbox ]; - darwinSystems.homebrew.casks = [ "virtualbox" ]; }; } diff --git a/modules/systems/all/shell/git.nix b/modules/systems/all/shell/git.nix deleted file mode 100644 index 023a4754..00000000 --- a/modules/systems/all/shell/git.nix +++ /dev/null @@ -1,93 +0,0 @@ -# Git configuration - reads user info and signing key from hostUsers -# Cross-platform system module (Linux + Darwin) -{ lib, config, system, ... }@args: - -let - isDarwin = system == "aarch64-darwin" || system == "x86_64-darwin"; - homeDir = if isDarwin then "/Users" else "/home"; - - # Get enabled users - enabledUsers = lib.filterAttrs (_: u: u.enable) config.hostUsers; - - # Build key path from key config (public key for signing) - keyPath = userName: key: - let - keyExt = { - "rsa" = "id_rsa"; - "ed25519" = "id_ed25519"; - "ecdsa" = "id_ecdsa"; - "ecdsa-sk" = "id_ecdsa_sk"; - }.${key.type}; - in - "${homeDir}/${userName}/.ssh/${key.name}_${keyExt}.pub"; - - # Get default git signing key for a user (key with "git" in purpose) - getGitKey = user: - let - keys = user.keys or [ ]; - gitKeys = lib.filter (k: lib.elem "git" (k.purpose or [ ])) keys; - defaultGitKey = lib.findFirst (k: k.isDefault or false) null gitKeys; - in - if defaultGitKey != null then defaultGitKey - else if gitKeys != [ ] then lib.head gitKeys - else null; - - # Build Git config for a user - mkUserGitConfig = cfg: userName: userCfg: - let - gitKey = getGitKey userCfg; - signingKeyPath = if gitKey != null then keyPath userName gitKey else null; - - clavisModuleEnabled = lib.my.shouldEnableModule { inherit config; modulePath = "home.security.clavis"; }; - clavisEnabled = isDarwin && ((userCfg.clavisAgent.enable or false) || clavisModuleEnabled); - bwEnabled = userCfg.bitwardenSshAgent.enable or false; - hasAgent = clavisEnabled || bwEnabled; - - # Dynamic signing via SSH agent when no static key is specified - useDynamicSigning = signingKeyPath == null && hasAgent; - in - { - programs.git = { - enable = true; - - signing = { - key = if signingKeyPath != null then signingKeyPath else null; - signByDefault = signingKeyPath != null || useDynamicSigning; - }; - - settings = { - user = { - name = userCfg.fullName; - email = userCfg.email; - }; - core.editor = cfg.editor; - push.autoSetupRemote = true; - pull.rebase = true; - init.defaultBranch = "main"; - gpg = { - format = "ssh"; - } // lib.optionalAttrs useDynamicSigning { - ssh.defaultKeyCommand = "sh -c 'echo key::$(ssh-add -L | head -1)'"; - }; - }; - }; - }; -in -lib.my.mkSystemModuleV2 args { - namespace = "all"; - description = "Git configuration (reads from hostUsers)"; - - extraOptions = { - editor = lib.mkOption { - type = lib.types.str; - default = "nvim"; - description = "Default Git editor"; - }; - }; - - module = cfg: { - home-manager.users = lib.mapAttrs - (userName: userCfg: mkUserGitConfig cfg userName userCfg) - enabledUsers; - }; -} diff --git a/modules/systems/darwin/ssh.nix b/modules/systems/darwin/ssh.nix index afdde399..d0b7c3a9 100644 --- a/modules/systems/darwin/ssh.nix +++ b/modules/systems/darwin/ssh.nix @@ -1,6 +1,6 @@ # macOS-specific SSH configuration (Keychain integration, Touch ID) # Darwin-only system module -{ lib, config, system, ... }@args: +{ lib, config, ... }@args: let # Get enabled users @@ -10,7 +10,11 @@ let mkUserDarwinSSHConfig = userName: userCfg: let allKeys = userCfg.keys or [ ]; - clavisModuleEnabled = lib.my.shouldEnableModule { inherit config; modulePath = "home.security.clavis"; }; + clavisModuleEnabled = lib.my.isFeatureEnabledForUser { + inherit config; + inherit userName; + modulePath = "home.security.clavis"; + }; clavisEnabled = (userCfg.clavisAgent.enable or false) || clavisModuleEnabled; in lib.optionalAttrs (allKeys != [ ]) ( diff --git a/modules/systems/linux/networking/tailscale.nix b/modules/systems/linux/networking/tailscale.nix index 91d4b63c..cfc441e3 100644 --- a/modules/systems/linux/networking/tailscale.nix +++ b/modules/systems/linux/networking/tailscale.nix @@ -1,5 +1,5 @@ # Tailscale VPN - Linux system module -{ lib, pkgs, config, ... }@args: +{ lib, pkgs, ... }@args: lib.my.mkSystemModuleV2 args { namespace = "linux"; @@ -43,12 +43,5 @@ lib.my.mkSystemModuleV2 args { # Firewall settings networking.firewall.checkReversePath = "loose"; - - # Systray app (for all enabled hostUsers) - home-manager.users = lib.mapAttrs - (name: _: { - home.packages = [ pkgs.tailscale-systray ]; - }) - (lib.filterAttrs (_: u: u.enable) config.hostUsers); }; } diff --git a/modules/systems/linux/sound.nix b/modules/systems/linux/sound.nix index d75b2ba5..09620faa 100644 --- a/modules/systems/linux/sound.nix +++ b/modules/systems/linux/sound.nix @@ -1,9 +1,6 @@ # Sound/Audio - Linux system module (PipeWire) -{ lib, pkgs, config, ... }@args: +{ lib, pkgs, ... }@args: -let - enabledUsers = lib.filterAttrs (_: u: u.enable) config.hostUsers; -in lib.my.mkSystemModuleV2 args { namespace = "linux"; description = "PipeWire audio with ALSA and PulseAudio emulation"; @@ -29,15 +26,5 @@ lib.my.mkSystemModuleV2 args { # Realtime priority for audio security.rtkit.enable = true; - - # Audio control apps for users - home-manager.users = lib.mapAttrs - (name: _: { - home.packages = with pkgs; [ - pavucontrol - pulseaudio - ]; - }) - enabledUsers; }; } diff --git a/profiles/desktop.nix b/profiles/desktop.nix index ca727bf9..90b62a7e 100644 --- a/profiles/desktop.nix +++ b/profiles/desktop.nix @@ -4,7 +4,7 @@ userModules = { home.browsers.enable = true; # Desktop infrastructure — intentionally NOT enabling home.desktop.enable broadly - # to avoid activating all WMs (awesomewm, gnome, hyprland). Pick a WM per host. + # to avoid activating all WMs (awesomewm, hyprland). Pick a WM per host. home.desktop.appearance.enable = true; home.desktop.services.enable = true; home.desktop.utils.enable = true; diff --git a/profiles/developer.nix b/profiles/developer.nix index 4e053f5d..82a0b6f8 100644 --- a/profiles/developer.nix +++ b/profiles/developer.nix @@ -12,7 +12,6 @@ system.all = { nix.settings.enable = true; nix.gc.enable = true; - shell.git.enable = true; shell.ssh.enable = true; }; } diff --git a/profiles/server.nix b/profiles/server.nix index 6c200c2f..a677eb72 100644 --- a/profiles/server.nix +++ b/profiles/server.nix @@ -12,7 +12,6 @@ fonts.enable = true; nix.settings.enable = true; nix.gc.enable = true; - shell.git.enable = true; shell.ssh.enable = true; };