diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..0ab01d7 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,15 @@ +# sops config — maps files → recipients (age keys) for +# encryption/decryption of hosts/*/secrets/*.yaml. +# +# Adding more NixOS hosts later: add another `&` entry keyed to that +# host's SSH host key and another creation_rule scoped to its secrets dir. + +keys: + - &brutcha_recovery age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl + - &astoria age1jzg3lvcacyrkhdqeu87fplqrsx6glrgfhjy9xtvapca8k22a5g9s6r22pl + +creation_rules: + - path_regex: hosts/astoria/secrets/.*\.yaml$ + age: + - *brutcha_recovery + - *astoria diff --git a/config/nvim/lua/plugins/init.lua b/config/nvim/lua/plugins/init.lua index e02f72a..56511ce 100644 --- a/config/nvim/lua/plugins/init.lua +++ b/config/nvim/lua/plugins/init.lua @@ -44,7 +44,7 @@ return { "nvim-treesitter/nvim-treesitter", priority = 52, build = function() - require("nvim-treesitter.install").update({ with_sync = true })() + require("nvim-treesitter.install").update({ with_sync = true }) end, }, { diff --git a/flake.lock b/flake.lock index 56ac3a8..7072f29 100644 --- a/flake.lock +++ b/flake.lock @@ -3,16 +3,16 @@ "brew-src": { "flake": false, "locked": { - "lastModified": 1784068757, - "narHash": "sha256-7KnV7rTlMpys+2J+TFVxUDDyKPLXFs4wIMtckMC72VM=", + "lastModified": 1785146564, + "narHash": "sha256-Sa7/HrfB04H32OJ7/ofxXjiZEbkWtCNOriONYYTL1OA=", "owner": "Homebrew", "repo": "brew", - "rev": "6bd951d96e7ebc54787799dba77bfb26ec956c4c", + "rev": "b2cfc03346d482f79886de108fee5dc49a6efc10", "type": "github" }, "original": { "owner": "Homebrew", - "ref": "6.0.11", + "ref": "6.0.13", "repo": "brew", "type": "github" } @@ -20,11 +20,11 @@ "claude-plugins-official": { "flake": false, "locked": { - "lastModified": 1784766795, - "narHash": "sha256-04vjOPpnieiLri1muM+h1/GHxne6XrPjP0lp62nxNY4=", + "lastModified": 1785715405, + "narHash": "sha256-m4bUT3bxxnhsayFsirHPd1sxhzPEGkG9yrQTYuULGN0=", "owner": "anthropics", "repo": "claude-plugins-official", - "rev": "e3e378cbbb205673a5d7254ded32679cafa6179d", + "rev": "909649d9b178d142201000c76715b5fc952818e3", "type": "github" }, "original": { @@ -33,14 +33,49 @@ "type": "github" } }, + "crane": { + "locked": { + "lastModified": 1781825982, + "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781152676, + "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=", + "owner": "nix-community", + "repo": "disko", + "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, "figma-plugin": { "flake": false, "locked": { - "lastModified": 1783964166, - "narHash": "sha256-ZkdCdDPNLPPKXa9bTIOw58jyTSv5hjMzx0kvygO6kxE=", + "lastModified": 1785413628, + "narHash": "sha256-metUsjmCvt5t2er2FWhKUGMFF/kLCS3PGlDwCLqMnL0=", "owner": "figma", "repo": "mcp-server-guide", - "rev": "07316dd2920d61303ca0e52812b31f5f341e7b15", + "rev": "ef474d181a6eca44b37722f839e8a7eb58d644ec", "type": "github" }, "original": { @@ -49,6 +84,65 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "lanzaboote", + "pre-commit", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "helium-flake": { "inputs": { "nixpkgs": [ @@ -56,11 +150,11 @@ ] }, "locked": { - "lastModified": 1784798039, - "narHash": "sha256-WwMugZH8Uy5vrtBeQypxqEuwPzMtQjGuAtt1qKpev9g=", + "lastModified": 1785461417, + "narHash": "sha256-p+AD3rx7jsoSWIUkexums+IjK6E/5njDlRTJ3ofl5TA=", "owner": "amaanq", "repo": "helium-flake", - "rev": "11e0f53dd6538e562fd3e085db34fb0503af9371", + "rev": "b968c3aa5c58b438a6b5ba4c9ea4fcdc2002fc42", "type": "github" }, "original": { @@ -76,11 +170,11 @@ ] }, "locked": { - "lastModified": 1784789275, - "narHash": "sha256-cgRTWuG+u1tBPhm01JrId2k0Bni09phVJ1Q0BZlRd7M=", + "lastModified": 1785531816, + "narHash": "sha256-vkMnV0JIyw+g/NmcfoajlGaAO+9a0ezia+FZohQJrik=", "owner": "nix-community", "repo": "home-manager", - "rev": "3b0e6bbd65869af1beadf5963a99befc179d209f", + "rev": "bf9ce9fec78f95f374e8dd3b503863a3ec128ebe", "type": "github" }, "original": { @@ -89,6 +183,30 @@ "type": "github" } }, + "lanzaboote": { + "inputs": { + "crane": "crane", + "nixpkgs": [ + "nixpkgs" + ], + "pre-commit": "pre-commit", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1782141370, + "narHash": "sha256-hqijVSEETttmo8Okql9/LG0Ua34hdciKW1a5zzlj8mU=", + "owner": "nix-community", + "repo": "lanzaboote", + "rev": "7c9a54a7f87b4539ddbd8bda09a8a5f5f9361aa9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "v1.1.0", + "repo": "lanzaboote", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -96,11 +214,11 @@ ] }, "locked": { - "lastModified": 1784500460, - "narHash": "sha256-UvORnAxTRHax7RG74W8Z2t4GvIkX6AjJ5kk0QlwZomo=", + "lastModified": 1785389976, + "narHash": "sha256-0tLW8Ff5yt8AH97jw4ZpFJ0OCJ122zIlgWGDmOfU/VU=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "57a3171f94705599a2499248ca5758d5eb47c0e0", + "rev": "15abb8c98f336cd8bd840d71059adebabe60bf04", "type": "github" }, "original": { @@ -115,11 +233,11 @@ "brew-src": "brew-src" }, "locked": { - "lastModified": 1784159664, - "narHash": "sha256-I/B6YoRLImHEqNWh8bs+tPjEDeteACeFhcLyoSMo1GE=", + "lastModified": 1785544760, + "narHash": "sha256-qV6OoNuly4ntqpCg7esIeJjUboxSnQNlLPxz+y5h9/o=", "owner": "zhaofengli", "repo": "nix-homebrew", - "rev": "842eeb863ecca0eeb463f7a814cdc51e1d925776", + "rev": "937ce52c7d046310571f3a070713804ead496843", "type": "github" }, "original": { @@ -128,13 +246,45 @@ "type": "github" } }, + "nixos-hardware": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1785232496, + "narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=", + "owner": "NixOS", + "repo": "nixos-hardware", + "rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixos-hardware", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1784555310, - "narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=", + "lastModified": 1767892417, + "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=", + "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1785715447, + "narHash": "sha256-4K0IGmbAOJnd/Hx2+CNEvx3ECrtYnsoaf4grI6gvZ/Q=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "421eebfd0ec7bccd4abe826ce62d7e6e83129493", + "rev": "243895692ae2a2fbd08a05141462c0cc0d3ca10f", "type": "github" }, "original": { @@ -144,15 +294,119 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1785692966, + "narHash": "sha256-vUfIeBEfpbAfZ5zjgIkYk7eHBeVfCYVjLbWnMkseYnk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "643809054d65fdd466a63e3155b8c498cb483c04", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1785747989, + "narHash": "sha256-uzfXp4xdXd+N7ISja44OD6UE7LsvoDtBeedLgFS5trk=", + "owner": "nix-community", + "repo": "NUR", + "rev": "9c2bcb321cb4a65cd9791fdfae7ff99787e9c22c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "pre-commit": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "lanzaboote", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { "claude-plugins-official": "claude-plugins-official", + "disko": "disko", "figma-plugin": "figma-plugin", "helium-flake": "helium-flake", "home-manager": "home-manager", + "lanzaboote": "lanzaboote", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", - "nixpkgs": "nixpkgs" + "nixos-hardware": "nixos-hardware", + "nixpkgs": "nixpkgs_2", + "nur": "nur", + "sops-nix": "sops-nix" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "lanzaboote", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782012058, + "narHash": "sha256-9mWUnReOUXfKjZuJAL/bAFH3LUyECTRtXgSNVjRw3UY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8534567325bd8a8d2928e6afd81e0a87d19efd3c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783174389, + "narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 80f5caf..15fce52 100644 --- a/flake.nix +++ b/flake.nix @@ -48,24 +48,64 @@ url = "github:amaanq/helium-flake"; inputs.nixpkgs.follows = "nixpkgs"; }; + + # --- NixOS-only inputs (used by astoria) --- + + # nixos-hardware — per-model hardware quirks (Dell XPS 13 9300) + # https://github.com/NixOS/nixos-hardware + nixos-hardware.url = "github:NixOS/nixos-hardware/master"; + + # disko — declarative disk partitioning (LUKS containers, Btrfs subvolumes) + # https://github.com/nix-community/disko + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # sops-nix — activation-time secrets, encrypted-in-repo + # https://github.com/Mic92/sops-nix + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # NUR — Nix User Repository, for rycee.firefox-addons (enhanced-h264ify, + # nextcloud-passwords). https://github.com/nix-community/NUR + nur.url = "github:nix-community/NUR"; + + # Lanzaboote — signed systemd-boot replacement (Secure Boot chain). Pinned + # to the release tag rather than main because it's a boot-chain-critical + # dependency and unpinned bumps can brick unattended updates. v1.1.0 + # (released 2026-06-22) is the first tag compatible with post-2026-06-09 + # nixpkgs where `boot.bootspec.enable` was removed. + # https://github.com/nix-community/lanzaboote + lanzaboote = { + url = "github:nix-community/lanzaboote/v1.1.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs@{ self, nix-darwin, home-manager, nix-homebrew, nixpkgs, ... }: let - # Custom utilities available globally as 'utils' - utils = import ./modules/lib/default.nix { lib = nixpkgs.lib; }; + # Custom utilities available globally as 'helpers'. Must not be named + # `utils` — NixOS's module framework injects its own internal `utils` arg. + helpers = import ./modules/lib/default.nix { lib = nixpkgs.lib; }; + + # Darwin host-wiring helpers (module-list and pkgs builders used by + # darwinConfigurations.* below) — see modules/lib/darwin-hosts.nix + darwinHosts = import ./modules/lib/darwin-hosts.nix { lib = nixpkgs.lib; }; # Base configuration shared across all systems # Enables flakes and sets up fundamental packages configuration = { pkgs, ... }: { # Enable flakes support globally to use nix flake commands # https://github.com/NixOS/nix/blob/master/doc/manual/rl-next.md - nix.settings.experimental-features = "nix-command flakes"; + nix.settings.experimental-features = ["nix-command" "flakes"]; # Flake-only setup — drop the legacy channels path from NIX_PATH # (silences "Nix search path entry .../channels does not exist"). nix.channel.enable = false; - + # Allow unfree packages nixpkgs.config.allowUnfree = true; @@ -100,75 +140,14 @@ # into the project's own .gitignore. See dev-shells/default.nix for # the full mechanism, inheritance rules, and future privacy options. - # Helper function to create home-manager configuration for a user - # Creates a module list that integrates home-manager with the system configuration - # and imports user-specific settings from hosts/${hostname}/home.nix - mkHomeConfig = { username, hostname, home, private ? null }: [ - home-manager.darwinModules.home-manager - { - # Set the user's home directory path - users.users.${username}.home = nixpkgs.lib.mkDefault home; - - # Use the system's nixpkgs instance for home-manager - home-manager.useGlobalPkgs = true; - # Install user packages to /etc/profiles instead of ~/.nix-profile. - # `private` is always present (null on hosts without one) so modules - # can pattern-match on it without triggering _module.args recursion. - home-manager.extraSpecialArgs = { - inherit inputs private; - rootDir = self; - utils = utils; - }; - - home-manager.useUserPackages = true; - - # When a file home-manager wants to manage already exists (e.g. - # KeePassXC writes its own keepassxc.ini before we declare it), - # move the existing file to `.backup` instead of aborting. - home-manager.backupFileExtension = "backup"; - - home-manager.users.${username} = { - home.username = username; - - imports = [ - ./hosts/${hostname}/home.nix - ]; - }; - } - ]; - - # Helper function to create nix-homebrew configuration for a user - # Creates a module list that integrates nix-homebrew with the system configuration - # nix-homebrew manages Homebrew installation itself, while nix-darwin's homebrew - # module manages packages declaratively. - # - # Parameters: - # - username: The user who owns the Homebrew installation - # - taps: Optional attribute set of Homebrew taps to manage declaratively - # - autoMigrate: Whether to automatically migrate existing Homebrew installations - # - # Homebrew integration approach: - # - Uses nix-homebrew to manage Homebrew installation itself - # - Uses nix-darwin's homebrew.* options to manage packages declaratively - # - Works with existing Homebrew installations via autoMigrate - mkHomebrewConfig = { username, taps ? { }, autoMigrate ? true }: [ - nix-homebrew.darwinModules.nix-homebrew - { - # Set the primary user for nix-darwin - system.primaryUser = username; - - nix-homebrew = { - enable = true; - enableRosetta = true; - user = username; - taps = taps; - mutableTaps = true; - autoMigrate = autoMigrate; - }; - } - ]; in { + # Re-export disko's CLI at the flake's own package output so the + # astoria README can invoke it as `sudo nix run '.#disko' -- ...` + # from the cloned repo — that hits our lock-pinned commit rather + # than `github:nix-community/disko` HEAD. + packages.x86_64-linux.disko = inputs.disko.packages.x86_64-linux.disko; + # macOS system configuration for makima (personal MacBook) darwinConfigurations.makima = let @@ -180,30 +159,22 @@ # (nix's pathExists under sudo is unreliable; let `import` fail with a # clearer file-not-found message if the file is missing) private = import "/Users/${username}/.config/dotfiles/private.nix"; - pkgs = import nixpkgs { - inherit system; - config = { allowUnfree = true; }; - overlays = [ - (final: prev: { - fish = prev.fish.overrideAttrs (old: { doCheck = false; }); - }) - (import ./pkgs { inherit utils; }) - ]; - }; + pkgs = darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; }; rootDir = self; in nix-darwin.lib.darwinSystem { inherit system; - specialArgs = { inherit utils pkgs rootDir private; }; + specialArgs = { inherit helpers pkgs rootDir private; }; modules = [ configuration ./hosts/${hostname}/default.nix - ] ++ mkHomeConfig { - inherit username hostname private; + ] ++ darwinHosts.mkHomeConfig { + inherit home-manager inputs rootDir helpers; + inherit username hostname private system; home = "/Users/${username}"; - } ++ mkHomebrewConfig { - inherit username; + } ++ darwinHosts.mkHomebrewConfig { + inherit nix-homebrew username; autoMigrate = true; }; }; @@ -221,39 +192,60 @@ # (nix's pathExists under sudo is unreliable; let `import` fail with a # clearer file-not-found message if the file is missing) private = import "/Users/${username}/.config/dotfiles/private.nix"; - pkgs = import nixpkgs { - inherit system; - config = { - allowUnfree = true; - }; - overlays = [ - (final: prev: { - fish = prev.fish.overrideAttrs (old: { - doCheck = false; - }); - }) - (import ./pkgs { inherit utils; }) - ]; - }; + pkgs = darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; }; rootDir = self; in nix-darwin.lib.darwinSystem { inherit system; - # Add utils to the nix flake specialArgs, make helpers like toARGB available in each module - specialArgs = { inherit utils pkgs rootDir private; }; + # Add helpers to the nix flake specialArgs, make helpers like toARGB available in each module + specialArgs = { inherit helpers pkgs rootDir private; }; modules = [ configuration ./hosts/${hostname}/default.nix - ] ++ mkHomeConfig { - inherit username hostname private; + ] ++ darwinHosts.mkHomeConfig { + inherit home-manager inputs rootDir helpers; + inherit username hostname private system; home = "/Users/${username}"; - } ++ mkHomebrewConfig { - inherit username; + } ++ darwinHosts.mkHomebrewConfig { + inherit nix-homebrew username; autoMigrate = true; }; }; + + # NixOS system configuration for astoria (Dell XPS 13 9300 thin-client) + # + # `inputs` MUST be threaded into BOTH system specialArgs (for + # hosts/astoria/{default,hardware}.nix which pattern-match `{ inputs, ... }:`) + # AND `home-manager.extraSpecialArgs` (for shared HM modules like + # modules/home/development/dev-shells/default.nix and claude-code.nix which + # also eagerly destructure `{ inputs, ... }:`). System specialArgs do NOT + # propagate into HM's module scope — the second wiring below is not + # optional; without it HM eval fails "attribute 'inputs' missing" at + # pattern-match, before any lib.mkIf gate can fire. + # + # `private = null` is safe: the modules that consume it all use `private ? + # null` at pattern-match. Passing null explicitly keeps parity with the + # darwin wiring pattern so any future extraction/dedup stays trivial. + nixosConfigurations.astoria = + let + system = "x86_64-linux"; + private = null; + rootDir = self; + in + nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = { inherit inputs rootDir private helpers; }; + modules = [ + configuration + ./hosts/astoria/default.nix + { + home-manager.extraSpecialArgs = { + inherit inputs private rootDir helpers; + hostSystem = system; + }; + } + ]; + }; }; } - - diff --git a/hosts/NB2123/home.nix b/hosts/NB2123/home.nix index 25178c1..04f0a2e 100644 --- a/hosts/NB2123/home.nix +++ b/hosts/NB2123/home.nix @@ -24,18 +24,30 @@ let CORP_FIGMA_ACCESS_TOKEN = "corp/figma-access-token"; }; - # Corp Claude Code marketplaces + plugin picks, sourced from private.nix + # Corp Claude Code marketplaces + plugin picks, sourced from private.nix. + # `programs.claude-code.plugins` is either-attrset-or-list; the base module + # in modules/home/development/claude-code.nix defines an attrset, so this + # override must too — the two forms can't merge. corpMarketplaces = lib.mapAttrs (_: cfg: fetchGit cfg) (private.claude.marketplaces or { }); - corpPlugins = map - (p: "${corpMarketplaces.${p.marketplace}}/${p.path}") + corpPluginList = map + (p: { + name = p.name or (baseNameOf p.path); + value = "${corpMarketplaces.${p.marketplace}}/${p.path}"; + }) (private.claude.plugins or [ ]); + corpPluginNames = map (p: p.name) corpPluginList; + corpPlugins = + if lib.length corpPluginNames != lib.length (lib.unique corpPluginNames) then + throw "corpPlugins: duplicate plugin name(s) in private.claude.plugins — set an explicit `name` on entries whose `path` basename collides" + else + lib.listToAttrs corpPluginList; in { home.stateVersion = "25.05"; imports = [ - ../../modules/home/darwin # darwin bundle (imports shared + darwin-only extras) + ../../modules/home ./registries.nix # NB2123-only npm/yarn corp registries ]; diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md new file mode 100644 index 0000000..1152464 --- /dev/null +++ b/hosts/astoria/README.md @@ -0,0 +1,443 @@ +# astoria — Dell XPS 13 9300 NixOS thin-client + +Sofa/bed/bath companion to a TV-connected Nobara desktop. Sway, Moonlight, +LibreWolf. TV inputs via AV receiver → single-scene Sunshine on the host. + +Design rationale (why Lanzaboote, why TPM-sealed cryptswap, why sops-nix over a +private flake, threat model) lives in the author's working notes outside the +repo. In-file: each module's header comment carries the "why" for its own +choices; this file is the operator's runbook. + +--- + +## Rebuild +- Preferred: `cd ~/git/dotfiles && sudo nixos-rebuild switch --flake .#astoria` +- Via /etc/nixos symlink: `sudo nixos-rebuild switch --flake /etc/nixos#astoria` +- From dev machine over LAN: `nixos-rebuild switch --flake .#astoria --target-host brutcha@astoria --use-remote-sudo` + +## Update +- Flake inputs: `nix flake update` (from dev machine, review lock diff, commit) +- Firmware: `fwupdmgr refresh && fwupdmgr get-updates && fwupdmgr update` + +## Rollback + +Two independent mechanisms — use the right one for the failure mode. + +**NixOS generation rollback** — for a `nixos-rebuild switch` that broke something: +- Lanzaboote boot menu at boot → pick a previous generation +- Or from CLI (still-booted system): `sudo nixos-rebuild switch --rollback` + +**Snapper rootfs rollback** — for non-`/nix/store` drift (files edited outside +the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live under +`/.snapshots//snapshot`, created at every boot by `snapper-boot.service`: +```bash +sudo snapper -c root list # inspect snapshots + timestamps +sudo snapper -c root diff .. # peek at what would change +sudo snapper -c root undochange ..0 # restore from snapshot N +sudo snapper -c root rollback # nuke current @root, replace with snapshot N — reboots into it +``` +`rollback` swaps subvolumes, destroying current @root — use `undochange` +instead for a few files. + +Note: Snapper's SUBVOLUME is `/` (@root only). `/nix`, `/home`, `/.snapshots` +are separate subvolumes, not covered — `/nix/store` is already immutable + +content-addressed; `/home` is user data Restic backs up. + +--- + +## Secrets (sops) + +astoria decrypts `hosts/astoria/secrets/astoria.yaml` at boot with its SSH +host key (as age, `&astoria` in `.sops.yaml`) or the off-host recovery key +(`&brutcha_recovery`) — either works alone. Schema: `hosts/astoria/secrets.nix`. + +Tooling docs: [sops-nix](https://github.com/Mic92/sops-nix) (the NixOS +module wiring `.sops.yaml` → `secrets.nix` → `/run/secrets`), +[sops](https://github.com/getsops/sops) (the encryption CLI), and +[age](https://github.com/FiloSottile/age) (the key format). + +--- + +## Dev-machine secrets (only needed for host-key rotation / disaster recovery) + +Generating astoria's secrets from scratch needs, once: +- SSH host key (age via `ssh-to-age`, `&astoria` recipient) — both halves to + `/etc/ssh/ssh_host_ed25519_key`(`.pub`) on the machine (Phase 3 step 3) and + to the vault (`"astoria SSH host key"`). +- Recovery age keypair (`age-keygen`, once ever, shared fleet-wide — rotate + with a new keypair + `sops updatekeys` on every host) → `&brutcha_recovery`; + private key to vault (`"astoria sops recovery"`). Shred discipline below. +- Login/cryptswap LUKS passphrases, a restic repo password, and a + `mkpasswd -m yescrypt` password hash — each its own vault entry + (`"astoria login"`, `"astoria cryptswap"`, `"astoria restic repo"`), + assembled into `hosts/astoria/secrets/astoria.yaml` per `secrets.nix`'s + schema, then `sops updatekeys hosts/astoria/secrets/astoria.yaml`, commit, + push. + +Tooling docs: [ssh-to-age](https://github.com/Mic92/ssh-to-age), +[age](https://github.com/FiloSottile/age), +[sops](https://github.com/getsops/sops). + +--- + +## Phase 3 — install + +Boot the [nix-community/nixos-images](https://github.com/nix-community/nixos-images) +unstable installer ISO, grab IP + root password from its QR/JSON, SSH in as +root. Everything below runs in that session (already root, no `sudo`) except +the host-key transfer, run from the dev machine. + +```bash +GITHUB_USER= +ASTORIA_IP= # from the installer's on-screen/QR/clipboard info +ssh "root@${ASTORIA_IP}" +``` + +### 1. Clone the flake + +```bash +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#git -- \ + clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles +cd /tmp/dotfiles +``` + +The minimal ISO's closure is bare and flakes aren't enabled by default — +fetch anything else the same way: +```bash +nix --extra-experimental-features 'nix-command flakes' run nixpkgs# -- ... +``` +Packages that come up needing this during install: `sops`, `ssh-to-age`, +`sbctl`, `pciutils` (for `lspci`), `stress-ng`, `s-tui`, +`linuxPackages.turbostat`. `sbctl` also needs `--disable-landlock` writing +outside its expected paths (step 4's `--export`) — it sandboxes itself with +Landlock by default. + +### 2. Partition + format + +This model defaults to **RAID (Intel RST)** in BIOS, hiding the NVMe drive +(`lspci` shows `RAID bus controller`, `dmesg` shows `Found 1 remapped NVMe +devices`). Fix: BIOS → System Configuration → **SATA Operation: AHCI**. No +config changes needed after — `nvme` in `boot.initrd.availableKernelModules` +already covers it. + +```bash +nix --extra-experimental-features 'nix-command flakes' run '.#disko' \ + -- --mode destroy,format,mount --flake .#astoria +``` + +`.#disko` resolves through the flake's own pinned +`packages.x86_64-linux.disko` re-export — no unpinned github ref, same code +as the rest of the flake. + +Prompts, in order: +1. `Type 'yes' to continue, anything else to abort:` — type literally `yes`. + **Don't paste a passphrase here** (anything except `yes` aborts before + formatting starts). Skip this prompt with `--yes-wipe-all-disks` if + re-running non-interactively. +2. **cryptswap** LUKS passphrase (2×) — from vault `"astoria cryptswap"`. +3. **cryptroot** LUKS passphrase (2×) — from vault `"astoria login"`. + +### 3. Transfer astoria's SSH host key from dev → installer + +From the dev machine (separate terminal from the root SSH session above), +scp both halves of the key over, install them, shred every transient copy +on both ends: +```bash +scp /tmp/astoria_host_key /tmp/astoria_host_key.pub "root@${ASTORIA_IP}:/tmp/" +ssh "root@${ASTORIA_IP}" 'mkdir -p /mnt/etc/ssh && \ + install -m 600 -o root -g root /tmp/astoria_host_key /mnt/etc/ssh/ssh_host_ed25519_key && \ + install -m 644 -o root -g root /tmp/astoria_host_key.pub /mnt/etc/ssh/ssh_host_ed25519_key.pub && \ + shred -u /tmp/astoria_host_key /tmp/astoria_host_key.pub' +shred -u /tmp/astoria_host_key /tmp/astoria_host_key.pub +``` + +### 4. Generate sbctl Secure Boot keys + +`nixos-install` signs the bootloader via Lanzaboote's installHook using these +keys, so they must exist at `/mnt/var/lib/sbctl` before install. + +```bash +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- create-keys --help # confirm flag names first +mkdir -p /mnt/var/lib/sbctl +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- --disable-landlock create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID +``` + +Gotchas: +- `--database-path` is a **file** path (writes a GUID file there), not a + directory. Passing a directory → EISDIR → no keys ever created. +- Older sbctl uses `--keydir` / `--pki-dir` instead. Check `--help` first. + +### 5. Install + +```bash +nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd +``` + +During activation: +- sops decrypts `astoria.yaml` using the host key from step 3. Hashed password + materializes at `/run/secrets-for-users/…` before user creation, so greetd + accepts login on first boot. +- Lanzaboote signs bootloader + kernel + initrd with the sbctl keys. +- cryptswap TPM keyslot doesn't exist yet — first boot prompts for the + disko-set passphrase. TPM enrollment happens in Phase 4c. + +### 6. Reboot + +cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd +→ user login → Sway. + +--- + +## Phase 4 — first-boot + Secure Boot + TPM + +> **Single-sitting rule**: do 4a → 4b → 4c back-to-back — SB is off and the +> ESP is writable by any live USB until 4b. PCR 7 tracks Secure Boot +> **policy state** (enrolled PK/KEK/db), not ESP file contents, so this +> window is about limiting physical-access time, not PCR 7 catching +> tampering. Once SB is on with only the legit keys enrolled, unsigned +> payloads won't boot anyway. ~30 minutes total. + +### 4a. Bring-up checks + +Still open as of the last session: + +- [ ] **Add the dev-machine ssh pubkey** to + `users.users.brutcha.openssh.authorizedKeys.keys` (edit + `hosts/astoria/default.nix` in the just-cloned dir, commit, rebuild) + — required before first `--target-host` rebuild. +- [ ] Manual first Restic backup: + `sudo systemctl start restic-backups-webdav.service`; journal shows + success. +- [ ] Password-vault LibreWolf extension: paste server URL, log in with your + vault account, unlock with the challenge password on first launch + (one-time, manual). + +### 4b. Enroll Secure Boot keys + +Enroll BEFORE turning SB on in BIOS — first SB-enabled boot must verify +Lanzaboote against enrolled keys. + +```bash +sudo sbctl status # Setup Mode: Enabled, Secure Boot: Disabled +sudo sbctl enroll-keys --microsoft # --microsoft: append MS certs to KEK+db (option ROMs) +sudo sbctl verify # every file: ✓ Signed +``` + +Reboot → BIOS → enable Secure Boot → save. Boots normally, still prompts for +both cryptroot + cryptswap passphrases (TPM not enrolled yet). + +Verify from the running system: +```bash +bootctl status | grep 'Secure Boot' # enabled +sudo sbctl verify +``` + +### 4c. Enroll TPM keyslot for cryptswap + +```bash +ls /sys/class/tpm/ # tpm0 should be there +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 +``` + +Prompts for the cryptswap passphrase. PCRs 0/2/7 = firmware code / extended +firmware code / SB state — key release requires all three unchanged. + +Reboot to verify: cryptroot passphrase (always), cryptswap unlocks SILENTLY +via TPM (no prompt). + +**Recovery if enrollment failed mid-step** (tpm2-tss error, DA-lockout, PCR +read error): the passphrase keyslot is UNAFFECTED — cryptswap still opens +via passphrase. Clean up with: +```bash +sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 # non-fatal if no slot +``` +Then retry the enroll command. If DA-locked: +```bash +sudo tpm2_dictionarylockout --clear-lockout # blank owner pw on freshly-cleared TPM +``` + +Verify keyslots: +```bash +sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) + slot N (tpm2) +``` + +### 4d. Lockdown check + +- [ ] `bootctl status` shows Secure Boot enabled, Setup Mode disabled. +- **BIOS admin password**: trade-off. Unset keeps TLP's charge-threshold + writes; set blocks Setup-Mode re-entry but may break them — test first. + PCR sealing only gates *TPM key release*: a hostile re-enrollment breaks + the seal → passphrase fallback next boot (a signal, not tamper detection). + +--- + +## Restore from backup +- Secrets already materialized on running astoria: `/run/secrets/rclone/webdav.conf` + + `/run/secrets/restic/repo-password` +- `sudo nix-shell -p restic rclone` — enters an interactive root shell with restic + + rclone on PATH. +- Already root inside; rclone needs `RCLONE_CONFIG` explicit (`HOME=/root` + won't find the sops config). Same for restic's password file. **Don't + wrap in another `sudo`** — it resets PATH and drops the nix-shell env. + Use plain `env`: + ```bash + env \ + RCLONE_CONFIG=/run/secrets/rclone/webdav.conf \ + RESTIC_PASSWORD_FILE=/run/secrets/restic/repo-password \ + restic -r rclone:webdav:restic-astoria snapshots + env \ + RCLONE_CONFIG=/run/secrets/rclone/webdav.conf \ + RESTIC_PASSWORD_FILE=/run/secrets/restic/repo-password \ + restic -r rclone:webdav:restic-astoria restore latest --target / + ``` + +## Password recovery (lost sudo password — machine still boots) +1. From a machine that has the recovery age key: `sops hosts/astoria/secrets/astoria.yaml` + — replace `users.brutcha.hashed-password` with a new `mkpasswd -m yescrypt` hash. + Commit + push. +2. On astoria: `sudo nixos-rebuild switch` (or `--target-host` from dev machine if + stuck at greetd). + +## Password recovery (fully bricked — installer rescue) +1. Boot NixOS installer USB. +2. `sudo cryptsetup luksOpen /dev/nvme0n1p3 cryptroot` # p3 = root; p2 = swap +3. `sudo mount -o subvol=@root /dev/mapper/cryptroot /mnt` +4. `sudo mount -o subvol=@nix /dev/mapper/cryptroot /mnt/nix` +5. `sudo mount -o subvol=@home /dev/mapper/cryptroot /mnt/home` +6. `sudo mount /dev/nvme0n1p1 /mnt/boot` +7. `sudo nixos-enter --root /mnt` +8. Do NOT `passwd brutcha` — `users.mutableUsers = false;` reverts it. Update sops + YAML from a trusted machine and rebuild. + +## Recovery age key — helper-device shred discipline + +The recovery age private key lives in your password vault. Any time you paste it out +of the vault onto a helper device to run `sops`, follow this discipline: + +1. On the helper device, mount a scratch tmpfs first: + - Linux: `SCRATCH=$(mktemp -d --tmpdir=/dev/shm astoria-recovery.XXXXX)` + - macOS: the default path lands on the boot APFS volume — `rm` frees the + inode but blocks linger, and Time Machine snapshots keep it ~24h. Use a + RAM disk: + `hdiutil attach -nomount ram://8192 | xargs -I{} diskutil erasevolume APFS 'ARamDisk' {}; SCRATCH=/Volumes/ARamDisk` +2. Confirm `shred` is available, then set the cleanup trap (before any + paste) — failures are reported, not silently swallowed: + ```bash + command -v shred || { echo "shred not found — do not paste yet" >&2; exit 1; } + trap ' + shred -u -- "$SCRATCH"/keys.txt \ + || printf "WARNING: shred failed — %s/keys.txt may still be readable\n" "$SCRATCH" >&2 + rm -rf "$SCRATCH" 2>/dev/null + diskutil eject ARamDisk 2>/dev/null + ' EXIT INT TERM + ``` +3. Paste the recovery age private key to `"$SCRATCH"/keys.txt`; `chmod 600 "$SCRATCH"/keys.txt` immediately. +4. `SOPS_AGE_KEY_FILE="$SCRATCH"/keys.txt sops hosts/astoria/secrets/astoria.yaml` + — decrypt, edit, save, then `sops updatekeys …` to re-encrypt. +5. Commit + push. Exit the shell — the trap fires and shreds the temp key. +6. Verify, from a fresh shell (`$SCRATCH` no longer exists once you've + exited the one that defined it): `ls -la /dev/shm/astoria-recovery.*` + (Linux) should show "No such file or directory". On macOS also verify + `tmutil listlocalsnapshots /` doesn't show a recent snapshot containing the file + (they roll off in ~24h; force-delete with `tmutil deletelocalsnapshots`). + +## LUKS passphrase change +```bash +sudo cryptsetup luksChangeKey /dev/nvme0n1p3 # cryptroot — also update `"astoria login"` in the vault +sudo cryptsetup luksChangeKey /dev/nvme0n1p2 # cryptswap fallback — also update its vault entry +``` +TPM keyslot is separate from the passphrase keyslot — changing the +passphrase doesn't touch the TPM binding. Rotate TPM only if needed (BIOS +updates, PCR changes). + +## TPM re-enrollment (after BIOS/firmware update — expected ~1-2× per year) + +Symptoms: first boot after a BIOS update prompts for the cryptswap passphrase +(fallback path) instead of TPM auto-unlock. `journalctl -b -u 'systemd-cryptsetup@luks\x2dswap.service'` +reports PCR mismatch. + +Fix: +```bash +sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 +``` +Prompted for the cryptswap passphrase to authorize. Reboot to verify silent unlock. + +## Secure Boot state audit (occasional) +```bash +sudo sbctl status # Setup Mode: Disabled, Secure Boot: Enabled +sudo sbctl verify # every EFI file: Signed +bootctl status | grep 'Secure' +``` +If `sbctl verify` shows unsigned files after a manual bootloader tweak: +`sudo sbctl sign -s ` per file, then `sudo nixos-rebuild switch`. + +## Full Secure Boot reset (rare — if keys get corrupted or you need to re-provision) + +1. Reboot into BIOS. Advanced Boot Options → Secure Boot → "Reset to Setup Mode" (or + "Delete All Keys"). +2. Boot back into NixOS. `sudo sbctl reset` — resets UEFI PK/KEK/db to Setup + Mode via an empty signature DB. **Doesn't touch `/var/lib/sbctl`** — + local keypair survives. +3. **Delete the local keypair** so step 4 doesn't silently no-op: + ```bash + sudo rm -rf /var/lib/sbctl/keys /var/lib/sbctl/GUID + ``` + `sbctl create-keys` won't overwrite an existing keydir — skip this and + the "reset" is a no-op. +4. `sudo sbctl create-keys` — new keypair, now that the keydir is empty. +5. `sudo nixos-rebuild switch` — re-signs the bootloader + kernel + initrd with the + new keys. +6. `sudo sbctl enroll-keys --microsoft` — enroll into UEFI. +7. Reboot into BIOS → enable Secure Boot. +8. **TPM state also invalidates** (PCR 7 changes): follow "TPM re-enrollment" above. + +## Battery care +Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement +(~€60, iFixit rating 4/10) recommended if capacity drops below 60 %. + +## Known gotchas +- iwlwifi power_save must stay off — turning it on drops Moonlight streams. +- `enhanced-h264ify` blocks AV1 on YouTube while leaving VP9 available; keeps + 4K/1440p playback that plain `h264ify` would silently drop. +- The inlined disko block at the bottom of `hardware.nix` owns the disk layout — + do NOT re-run `nixos-generate-config`. +- `users.mutableUsers = false;` — never fix passwords with `passwd`; always go + through sops. +- **DRM sites (Netflix / Prime / Disney+) don't work on astoria** — LibreWolf ships + Widevine disabled (default-off, user-toggleable). Netflix on Linux additionally + caps at ~720p. Watch DRM content from the Nobara desktop via Moonlight instead. +- The password-vault LibreWolf extension is declaratively installed but not + configured — set server URL, log in with your vault account, unlock with the + challenge password on first launch (manual, one-time). +- **If suspend-then-hibernate fails to wake on schedule**: add + `rtc_cmos.use_acpi_alarm=1` to `boot.kernelParams`. Redundant on this + hardware (kernel auto-quirks it), so hardware.nix omits it — documented + fallback if a BIOS update breaks the auto-quirk. +- **Cryptswap prompts for a passphrase after a BIOS update**: expected — TPM PCRs + changed, sd-cryptsetup fell back to the passphrase keyslot. Re-run TPM enrollment + (see "TPM re-enrollment"). One-time-per-BIOS-bump friction. +- **`sbctl verify` shows unsigned files**: something touched the ESP outside + `nixos-rebuild switch`. `sudo sbctl sign -s ` and rebuild; check the ESP + wasn't manually edited. +- **Dell Expert Key Management: "Reset All Keys" vs "Delete All Keys"** — + opposite actions. **Reset** restores factory MS/OEM keys (wrong). + **Delete** clears everything into Setup Mode (correct). The "N changes" + counter doesn't track Delete — trust `sbctl status` after reboot instead. + Also: SB-on with no PK enrolled can throw a "no bootable devices" screen + on some BIOS revisions — keep SB off until `sbctl enroll-keys` succeeds, + then enable it last. + +## References + +- [NixOS](https://nixos.org) +- [home-manager](https://github.com/nix-community/home-manager) +- [sops-nix](https://github.com/Mic92/sops-nix) +- [disko](https://github.com/nix-community/disko) +- [lanzaboote](https://github.com/nix-community/lanzaboote) +- [snapper](https://github.com/openSUSE/snapper) +- [TLP](https://linrunner.de/tlp/) +- [sbctl](https://github.com/Foxboron/sbctl) +- [nix-community/nixos-images](https://github.com/nix-community/nixos-images) +- [Moonlight](https://moonlight-stream.org) +- [LibreWolf](https://librewolf.net) diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix new file mode 100644 index 0000000..020c17c --- /dev/null +++ b/hosts/astoria/default.nix @@ -0,0 +1,219 @@ +{ config, pkgs, lib, inputs, helpers, ... }: +# +# astoria — Dell XPS 13 9300 NixOS thin-client. Sway sole session, +# Moonlight-first, LibreWolf, greetd, no autologin. +# +# System composition + non-hardware config. Hardware knobs + disko live +# in ./hardware.nix. Sops secret declarations in ./secrets.nix. +# +{ + imports = [ + inputs.sops-nix.nixosModules.sops + inputs.home-manager.nixosModules.home-manager + ./hardware.nix + ./secrets.nix + ./sudoers.nix + ]; + + nixpkgs.config.allowUnfree = true; # required by hardware.enableAllFirmware + nixpkgs.overlays = [ inputs.nur.overlays.default (import ../../pkgs { inherit helpers; }) ]; + + networking.hostName = "astoria"; + + # --- Locale / timezone / console --- + time.timeZone = "Europe/Prague"; + i18n.defaultLocale = "en_US.UTF-8"; + i18n.extraLocales = [ "cs_CZ.UTF-8/UTF-8" ]; + i18n.extraLocaleSettings = { + LC_TIME = "cs_CZ.UTF-8"; + LC_NUMERIC = "cs_CZ.UTF-8"; + }; + console.keyMap = "us"; # LUKS prompt reads from this + + # --- User (password from sops — see ./secrets.nix) --- + users.mutableUsers = false; + users.users.brutcha = { + isNormalUser = true; + description = "brutcha"; + extraGroups = [ "wheel" "video" "input" "render" "audio" "networkmanager" ]; + shell = pkgs.zsh; + hashedPasswordFile = config.sops.secrets."users/brutcha/hashed-password".path; + # Fill in Nobara's pubkey after first boot (README Phase 4a). + openssh.authorizedKeys.keys = [ + # "ssh-ed25519 AAAA... brutcha@nobara" + ]; + }; + programs.zsh.enable = true; # REQUIRED when user shell = pkgs.zsh + + # --- Sway via greetd tuigreet (no autologin — password muscle memory) --- + programs.sway = { + enable = true; + wrapperFeatures.gtk = true; + }; + services.greetd = { + enable = true; + settings.default_session = { + command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd sway"; + user = "greeter"; + }; + }; + + # --- XDG portals + polkit --- + # `common.default` catches processes that spawn before Sway exports + # XDG_CURRENT_DESKTOP — without it portal resolution is nondeterministic. + xdg.portal = { + enable = true; + wlr.enable = true; + extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; + config = { + sway.default = lib.mkForce [ "wlr" "gtk" ]; + common.default = [ "wlr" "gtk" ]; + }; + }; + # Backs the portal's color-scheme signal (Ghostty/LibreWolf dark mode) — + # gtk-application-prefer-dark-theme alone doesn't reach it. + programs.dconf.enable = true; + security.polkit.enable = true; + + # --- System packages --- + environment.systemPackages = with pkgs; [ + intel-gpu-tools # intel_gpu_top for VA-API verification + brightnessctl # sway XF86MonBrightness keybinds + sbctl # Secure Boot key management + tpm2-tools # TPM inspection + re-enrollment + pavucontrol # waybar pulseaudio click target + ]; + # brightnessctl's udev rule gives `video` group write access to + # /sys/class/backlight — services.udev scans only .packages (NOT + # environment.systemPackages), so it must be listed here explicitly. + services.udev.packages = [ pkgs.brightnessctl ]; + environment.sessionVariables = { + MOZ_ENABLE_WAYLAND = "1"; + NIXOS_OZONE_WL = "1"; + LIBVA_DRIVER_NAME = "iHD"; # Ice Lake Gen 11+ + }; + + # --- Audio (PipeWire) --- + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + wireplumber.enable = true; + extraConfig.pipewire."92-video-streaming"."context.properties" = { + "default.clock.rate" = 48000; + "default.clock.allowed-rates" = [ 44100 48000 ]; + "default.clock.quantum" = 512; + "default.clock.min-quantum" = 128; + "default.clock.max-quantum" = 2048; + }; + }; + + # --- Credential agents --- + # NOT setting programs.ssh.startAgent: gnome-keyring-daemon's ssh + # component already exports SSH_AUTH_SOCK; enabling both spawns two + # agents that race for the socket. + services.gnome.gnome-keyring.enable = true; + security.pam.services.login.enableGnomeKeyring = true; + + # --- SSH (for --target-host rebuilds from LAN) --- + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; + + # --- Nix --- + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.trusted-users = [ "brutcha" ]; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + nix.optimise.automatic = true; + + # --- Disk / filesystem maintenance --- + services.fstrim.enable = true; + services.btrfs.autoScrub.enable = true; + + services.journald.extraConfig = '' + SystemMaxUse=1G + MaxRetentionSec=1month + ''; + + # --- Sway ancillary services --- + services.upower.enable = true; + services.udisks2.enable = true; + services.gvfs.enable = true; + services.smartd = { + enable = true; + notifications.mail.enable = false; # no MTA — don't spam journal with sendmail failures + notifications.wall.enable = true; + }; + services.avahi = { enable = true; nssmdns4 = true; }; + + # --- Snapper (on-boot snapshot only, no timeline) --- + # Without snapshotRootOnBoot (or a timeline / activation script) the + # snapper module installs the daemon + cleanup timer but nothing creates + # snapshots — NUMBER_LIMIT would be meaningless. + services.snapper.snapshotRootOnBoot = true; + services.snapper.configs.root = { + SUBVOLUME = "/"; + ALLOW_USERS = [ "brutcha" ]; + TIMELINE_CREATE = false; + TIMELINE_CLEANUP = false; + NUMBER_CLEANUP = true; + NUMBER_LIMIT = 20; + }; + services.snapper.cleanupInterval = "1d"; + + programs.nix-index.enable = true; + programs.command-not-found.enable = false; + + # --- Restic backups (secrets from sops) --- + # Daily backup, weekly prune. `initialize = true` on prune too — idempotent + # (`restic cat config || restic init`), so prune doesn't fail-loop if the + # daily backup never bootstrapped the repo. + services.restic.backups.webdav = { + initialize = true; + repository = "rclone:webdav:restic-astoria"; + paths = [ + "/home/brutcha/.ssh" + "/home/brutcha/.local/share/keyrings" + "/home/brutcha/.zsh_history" + ]; + extraBackupArgs = [ "--one-file-system" ]; + passwordFile = config.sops.secrets."restic/repo-password".path; + rcloneConfigFile = config.sops.secrets."rclone/webdav.conf".path; + timerConfig = { OnCalendar = "daily"; Persistent = true; RandomizedDelaySec = "30m"; }; + }; + services.restic.backups.webdav-prune = { + initialize = true; + repository = "rclone:webdav:restic-astoria"; + passwordFile = config.sops.secrets."restic/repo-password".path; + rcloneConfigFile = config.sops.secrets."rclone/webdav.conf".path; + paths = []; + timerConfig = { OnCalendar = "weekly"; Persistent = true; RandomizedDelaySec = "1h"; }; + pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 12" "--keep-within 30d" ]; + }; + + # /home/brutcha/git/dotfiles is populated in README Phase 4a; broken + # symlink until then (cosmetic warning only). + environment.etc.nixos.source = "/home/brutcha/git/dotfiles"; + + # --- Home Manager --- + # `home-manager.extraSpecialArgs` (including `inputs` + `hostSystem`) is + # wired from flake.nix — shared HM modules destructure `{ inputs, ... }:` + # at pattern-match, and modules/home/default.nix uses `hostSystem` to + # select the platform sub-bundle. + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.backupFileExtension = "backup"; + home-manager.users.brutcha = import ./home.nix; + + system.stateVersion = "26.11"; +} diff --git a/hosts/astoria/hardware.nix b/hosts/astoria/hardware.nix new file mode 100644 index 0000000..5f1ce03 --- /dev/null +++ b/hosts/astoria/hardware.nix @@ -0,0 +1,192 @@ +{ config, pkgs, lib, inputs, ... }: +# +# astoria hardware — Dell XPS 13 9300 (i7-1065G7 Ice Lake, 4K UHD+, 16 GB). +# +# Bootloader (Lanzaboote + Secure Boot), kernel + initrd + LUKS + resume, +# Wi-Fi, Bluetooth, TLP, thermal, sleep/hibernate, hardware.graphics, +# firmware, and the disko partition config inlined at the bottom. +# +{ + imports = [ + # https://github.com/NixOS/nixos-hardware/tree/master/dell/xps/13-9300 + inputs.nixos-hardware.nixosModules.dell-xps-13-9300 # psmouse blacklist + i2c-designware sleep-resume + QCA6390 fw + # https://github.com/nix-community/disko + inputs.disko.nixosModules.disko + # https://github.com/nix-community/lanzaboote + inputs.lanzaboote.nixosModules.lanzaboote + ]; + + # --- Bootloader (Lanzaboote / Secure Boot) --- + # Two-step key lifecycle (README Phase 3 step 4 + Phase 4b step 2): + # `sbctl create-keys` at install time → local keypair under pkiBundle; + # `sbctl enroll-keys --microsoft` at first boot → enroll into UEFI while + # in Setup Mode. --microsoft appends MS certs to KEK+db only (PK stays + # under our key), so signed option ROMs still load. + boot.loader.systemd-boot.enable = lib.mkForce false; # Lanzaboote sets boot.loader.external.enable = true + boot.loader.efi.canTouchEfiVariables = true; + boot.lanzaboote = { + enable = true; + pkiBundle = "/var/lib/sbctl"; + configurationLimit = 30; + }; + + # --- Kernel --- + boot.kernelPackages = pkgs.linuxPackages; + # nixos-hardware appends `mem_sleep_default=deep` unconditionally, but this + # box's firmware is s2idle-only → `systemctl suspend` returns -EINVAL. + # Kernel is last-wins for `mem_sleep_default=`, so `mkAfter` overrides. + # `mkMerge` (not two `boot.kernelParams = [...]` — Nix parser rejects + # duplicate attr paths before eval, so `mkAfter` on a second decl doesn't + # rescue it). + boot.kernelParams = lib.mkMerge [ + [ "resume=/dev/mapper/luks-swap" ] # matches disko mapper (see §disko) + (lib.mkAfter [ "mem_sleep_default=s2idle" ]) + ]; + + # --- Initrd (systemd stage 1) --- + boot.initrd.systemd.enable = true; + boot.initrd.availableKernelModules = [ + # No microSD reader on the 9300 (dropped after 9370). sd_mod stays — + # usb_storage routes external USB via SCSI. + "xhci_pci" "thunderbolt" "vmd" "nvme" "usb_storage" "sd_mod" + ]; + # 3840x2400 framebuffer text at ~6pt is unreadable at the LUKS prompt. + console.earlySetup = true; + console.font = "ter-v32b"; + console.packages = [ pkgs.terminus_font ]; + + # --- LUKS: cryptroot = passphrase, cryptswap = TPM auto-unlock --- + # `device` + `allowDiscards` come from disko; only the additive TPM opt + # goes here (list-merge). TPM keyslot enrolled post-install via + # `systemd-cryptenroll` (README Phase 4c); passphrase set at disko time + # remains as fallback. + boot.initrd.luks.devices."luks-swap" = { + crypttabExtraOpts = [ "tpm2-device=auto" ]; + }; + + # --- Wi-Fi (AX201 iwlwifi tuning, confirmed via lspci -nn / dmesg) --- + boot.extraModprobeConfig = '' + options iwlwifi power_save=0 + ''; + networking.networkmanager.enable = true; + networking.networkmanager.wifi.powersave = false; + networking.networkmanager.wifi.backend = "wpa_supplicant"; + + # --- Bluetooth --- + hardware.bluetooth.enable = true; + hardware.bluetooth.powerOnBoot = false; + services.blueman.enable = true; + + # --- Power --- + services.power-profiles-daemon.enable = false; # TLP owns this + # https://linrunner.de/tlp/ + # TLP's Dell plugin writes `charge_types = Custom` via dell-smbios; that + # write silently fails when a BIOS admin password is set → thresholds + # never take effect. Keep BIOS admin password UNSET (README Phase 2). + services.tlp = { + enable = true; + settings = { + CPU_SCALING_GOVERNOR_ON_BAT = "powersave"; + CPU_ENERGY_PERF_POLICY_ON_BAT = "power"; + CPU_ENERGY_PERF_POLICY_ON_AC = "balance_performance"; + CPU_BOOST_ON_BAT = 0; + CPU_BOOST_ON_AC = 1; + CPU_HWP_DYN_BOOST_ON_BAT = 0; + # Silent no-op if /sys/firmware/acpi/platform_profile_choices is empty + # (verify-hardware.sh reports). + PLATFORM_PROFILE_ON_BAT = "cool"; + INTEL_GPU_MIN_FREQ_ON_BAT = 100; + INTEL_GPU_MAX_FREQ_ON_BAT = 750; + RUNTIME_PM_ON_BAT = "auto"; + PCIE_ASPM_ON_BAT = "powersupersave"; + WIFI_PWR_ON_BAT = "off"; + START_CHARGE_THRESH_BAT0 = 60; + STOP_CHARGE_THRESH_BAT0 = 80; + }; + }; + # nixos-hardware/dell/xps/13-9300 already mkDefault-enables thermald and + # fwupd; explicit `true` here for intent clarity (no-op override). + services.thermald.enable = true; + # https://github.com/erpalma/throttled — clears BIOS PL1/PL2 override on Ice Lake + services.throttled.enable = true; + + # --- Lid / suspend: suspend-then-hibernate (30 min) --- + services.logind.settings.Login = { + HandleLidSwitch = "suspend-then-hibernate"; + HandleLidSwitchExternalPower = "suspend-then-hibernate"; + HandleLidSwitchDocked = "ignore"; + HandlePowerKey = "suspend-then-hibernate"; + }; + # 26.11 removed `systemd.sleep.extraConfig` (mkRemovedOptionModule); attrs + # under `.settings.Sleep` become sleep.conf keys verbatim. + systemd.sleep.settings.Sleep = { + HibernateDelaySec = "30min"; + SuspendState = "mem"; + HibernateMode = "platform"; + }; + + # --- Graphics --- + # intel-compute-runtime meta says "12th Gen and newer" — Gen 11 unsupported. + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ intel-media-driver ]; + }; + + # --- Firmware --- + services.fwupd.enable = true; + hardware.enableAllFirmware = true; + + # --- Disk layout (disko, declarative) --- + # Inlined here so all HW-topology declarations live in one file. `device` + # on each LUKS entry, `allowDiscards`, and the auto-generated + # `boot.initrd.luks.devices.` entries come from disko — setting + # them again in boot.initrd.luks would conflict. + disko.devices.disk.main = { + device = "/dev/nvme0n1"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "4G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + cryptswap = { + size = "20G"; + label = "cryptswap"; + content = { + type = "luks"; + name = "luks-swap"; # matches boot.initrd.luks.devices.luks-swap + resume= + settings.allowDiscards = true; + content.type = "swap"; + }; + }; + cryptroot = { + size = "100%"; # auto-priority 9001 → placed last + label = "cryptroot"; + content = { + type = "luks"; + name = "cryptroot"; + settings.allowDiscards = true; + content = { + type = "btrfs"; + extraArgs = [ "-L" "nixos" "-f" ]; + subvolumes = { + "@root" = { mountpoint = "/"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@nix" = { mountpoint = "/nix"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@home" = { mountpoint = "/home"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@snapshots" = { mountpoint = "/.snapshots"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/hosts/astoria/home.nix b/hosts/astoria/home.nix new file mode 100644 index 0000000..cf09e17 --- /dev/null +++ b/hosts/astoria/home.nix @@ -0,0 +1,68 @@ +{ config, pkgs, ... }: +# +# astoria — home-manager entry for `brutcha`. +# +# Toggles the fleet-standard `home.apps.*` options; modules/home/default.nix +# picks the linux sub-bundle (sway/waybar/mako/fuzzel/screenshot/thunar/ +# librewolf/moonlight/imv) via `hostSystem`. +# +{ + imports = [ ../../modules/home ]; + + home.apps = { + development = { + ghostty.enable = true; + git.enable = true; + claude-code.enable = true; + }; + internet.librewolf.enable = true; + media.moonlight.enable = true; + media.imv.enable = true; + filemanager.thunar.enable = true; + windowManager = { + sway.enable = true; + waybar.enable = true; + mako.enable = true; + fuzzel.enable = true; + screenshot.enable = true; + }; + }; + + home.stateVersion = "26.11"; + home.username = "brutcha"; + home.homeDirectory = "/home/brutcha"; + + # --- GTK theming --- + # Cross-cutting; not owned by any single app module. + # `tweakVariants = ["black"]` modifies the theme's internal palette but + # does NOT change the folder name (install.sh: "${name}${theme}${color}${size}${ctype}" + # → "Tokyonight-Dark"). + gtk = { + enable = true; + theme = { + name = "Tokyonight-Dark"; + package = pkgs.tokyonight-gtk-theme.override { + colorVariants = [ "dark" ]; + sizeVariants = [ "standard" ]; + tweakVariants = [ "black" ]; + }; + }; + iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-icon-theme; }; + cursorTheme = { name = "Bibata-Modern-Ice"; package = pkgs.bibata-cursors; }; + # Matches the Nerd Font used across sway/waybar/mako/fuzzel. + font = { name = "JetBrainsMonoNL Nerd Font"; size = 10; }; + gtk3.extraConfig.gtk-application-prefer-dark-theme = 1; + }; + + # Portal color-scheme signal — what Ghostty/LibreWolf actually consult + # for dark mode on Wayland, unlike the GTK3-only setting above. + dconf.settings."org/gnome/desktop/interface" = { + color-scheme = "prefer-dark"; + }; + + # File-sync client. Server URL + creds set on first launch. + services.nextcloud-client = { + enable = true; + startInBackground = true; + }; +} diff --git a/hosts/astoria/secrets.nix b/hosts/astoria/secrets.nix new file mode 100644 index 0000000..6778be2 --- /dev/null +++ b/hosts/astoria/secrets.nix @@ -0,0 +1,46 @@ +{ config, lib, ... }: +# +# astoria — sops-nix (https://github.com/Mic92/sops-nix) secret declarations. +# +# `neededForUsers = true` materializes the hashed-password secret into +# /run/secrets-for-users BEFORE useradd runs. Without it, the user gets +# created with a locked shadow entry — silent soft-lock, recovery via +# installer USB only. +# +# The assertion below guards against enabling systemd-sysusers or userborn: +# on those paths sops-install-secrets-for-users' Before= ordering is soft, +# and a decrypt failure would let sysusers create the user with a `!` +# shadow entry (same soft-lock shape). +# +{ + assertions = [ + { + assertion = !(config.systemd.sysusers.enable or false) + && !((config.services.userborn or {}).enable or false); + message = '' + astoria: systemd.sysusers.enable and services.userborn.enable must both + stay false while users.mutableUsers = false and hashedPasswordFile is + sops-managed — the systemd-sysusers/userborn path uses soft ordering + that silently soft-locks the user on decrypt failure. See secrets.nix. + ''; + } + ]; + + sops = { + defaultSopsFile = ./secrets/astoria.yaml; + # age.sshKeyPaths defaults to services.openssh.hostKeys filtered to + # ed25519 (see sops-nix modules/sops/default.nix); openssh is enabled + # at the system level so no explicit setting needed. + + secrets = { + "users/brutcha/hashed-password" = { + neededForUsers = true; + }; + "restic/repo-password" = { }; + "rclone/webdav.conf" = { }; + # tailscale/authkey — add when mesh VPN goes in (plan §7). Declared + # here would require the yaml key to exist too, else sops-install-secrets + # aborts activation. + }; + }; +} diff --git a/hosts/astoria/secrets/astoria.yaml b/hosts/astoria/secrets/astoria.yaml new file mode 100644 index 0000000..f9e00dd --- /dev/null +++ b/hosts/astoria/secrets/astoria.yaml @@ -0,0 +1,31 @@ +users: + brutcha: + hashed-password: ENC[AES256_GCM,data:nnIbGx4cYYgD13nuz7Gqc8qst2h2MrvFrvPWH9MFxGRYKxOcOuNvvMdIDskaTsdoVP7GVqBOQv15rDp4yXQoSss7E6EUp5ToRg==,iv:sxgMcvB9jvNboXj28HNglHiHe2+gnlMqveiV8JVws/A=,tag:WhkixZLIw+r7t8EB10LWdw==,type:str] +restic: + repo-password: ENC[AES256_GCM,data:GBLQNrOIbqFXn2dkTW1c3nCyHs6TQslroeTINz9V6foYRPtuyZyOWGJjTdfPlVMe3/N1MJpdSfOUE+Oo+ToySw==,iv:1OFCFLJE41XZsnY8p5lu/pPsA/YDYrLuArUn4YBAJcM=,tag:08uZznAEbcdsuWmzbSHQ5Q==,type:str] +rclone: + webdav.conf: ENC[AES256_GCM,data:ZlYE5N1B0+tvHu8R1LeyPt52oLUJRD61NaN8NVAqTrGo4g8xUTz2tYNujV3jU20D73GslILnFxb5OFWkaNmPrwABIcDvDKINbKd+lIQIvJ1ls9P/IA6PVrbuTYJoNtqw19+Mv9AzyPSbKN63jOBUDJ0QMbXZeJuUN7OE3eIDWnIjTAP0PXxrXCv5VN4UANaG6NeJsljA25vC3V00E91XlyY2vvuewuJ5JS63cvMnpzrPcdO4w0Fk99lo,iv:E04kboQaKlLGE6B483RJO7FCxhuR5m7JW0kwo+mBCko=,tag:ScpYo2W2BeSC5+4k+9YoKg==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWm1iRjB0RXhERENTNFNJ + eGdSeHJqd2QvTEtnbWEvMXBPZ0QrN0xBS0FVCnBqS2dnbkZVSkIrMEtHMG5oWUgx + WTl2Vkc1KzhzSEw0ZUg4ZlBnZVFXNHcKLS0tIEFmSUFNR1JSbzZJaTlJRXFNRmRR + ZitmeDgyODFwQzZ0TW1pbVVDOWkyY28KQS1g4hjAU1F1mVExFiq9cOQMtnkK6BAg + rDQzYfMVYXqQVGhVeZ02dCiO3BWj5w89mQh7+MFl/85rFDJhkDSQQQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzdU05YWFlK0ZSMEZ3Wk1L + em1hQzFuQ3hWTXpLdy9DSnBmZkNPZU4wWmdBCjEyWkJ6RVJkdzJNUExpSm5UQ0M1 + RERhN2VrM3pMWDRsL21saWdmNUFyQmsKLS0tIFJvLzJTeEVpbzlKcUlJWlVtQkMx + d01DZFBzZVhTOGxxUFhrVlB3YlRxZk0KA+X0VEYqtSMWoqS1ItjK6A/GUxdzbklW + KZiocG6UyP9LB38qPpUuU39hFvfuszqs57wK73zEPpju7a21GXyNaA== + -----END AGE ENCRYPTED FILE----- + recipient: age1jzg3lvcacyrkhdqeu87fplqrsx6glrgfhjy9xtvapca8k22a5g9s6r22pl + lastmodified: "2026-08-02T10:26:49Z" + mac: ENC[AES256_GCM,data:PDqPrKiVibGhZG7Crs0MZFA89LDDe2fRYlqk6puaXWwLFEj4BMSHhBOvrZJ5dKUZiupPlhBrsZbAUblrpsmyI+no/zZekOE+VqliU2YfHA2JpFbhdfR/ipGjCvGQUii3GhUUhAxCLdTjDlPpn6BFDf8UwEM+gdEY4HSHvQMgttA=,iv:kz1VNg5eY7ZtKoNB28LVX5fp4Z3JJKVMoMw4yYwFJwA=,tag:OOkOvBE2ZPcdNhakW2yY5A==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/hosts/astoria/sudoers.nix b/hosts/astoria/sudoers.nix new file mode 100644 index 0000000..6f546a3 --- /dev/null +++ b/hosts/astoria/sudoers.nix @@ -0,0 +1,9 @@ +{ lib, ... }: +# NOPASSWD for these two exact rebuild commands only — no wildcard, so a +# passwordless call can't smuggle in an arbitrary --flake path. +{ + security.sudo.extraConfig = lib.mkAfter '' + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake .\#astoria + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos\#astoria + ''; +} diff --git a/hosts/makima/home.nix b/hosts/makima/home.nix index 464c354..061187d 100644 --- a/hosts/makima/home.nix +++ b/hosts/makima/home.nix @@ -11,8 +11,8 @@ # https://nix-community.github.io/home-manager/index.xhtml#sec-install-nix-darwin-module home.stateVersion = "25.05"; - # https://nix-community.github.io/home-manager/ - imports = [ ../../modules/home/darwin ]; + # reached via the universal bundle. + imports = [ ../../modules/home ]; home.apps = { development = { diff --git a/modules/home/darwin/default.nix b/modules/home/darwin/default.nix index 69aedd6..e4b35b1 100644 --- a/modules/home/darwin/default.nix +++ b/modules/home/darwin/default.nix @@ -1,22 +1,22 @@ { lib, pkgs, ... }: # -# Darwin bundle — imports the shared modules + darwin-specific extras + -# LaunchServices registration. Non-darwin hosts import ./default.nix directly. +# Darwin bundle — darwin-only categories. Shared modules +# (theme/fonts/shell/development) come from the universal bundle in +# ../default.nix; don't re-import from here — the cycle overflows the +# stack before `filterModules`'s dedup runs. # { imports = [ - ../default.nix # shared: theme, fonts, shell, development - ./development.nix # darwin dev extras: lazydocker, xcbuild - ./internet # davmail, helium - ./media # obs-studio (scene + profile) - ./security # keepass - ./window-manager # aerospace, sketchybar, jankyborders + ./development.nix + ./internet + ./media + ./security + ./window-manager ]; - # home-manager symlinks Mac .app bundles under ~/Applications/Home Manager Apps/, - # but LaunchServices doesn't recurse into that subdirectory. Re-register each - # bundle's resolved nix-store target so `open -a`, Spotlight, and Launchpad - # find them (`lsregister -f` needs a real path, not a symlink). + # LaunchServices doesn't recurse into ~/Applications/Home Manager Apps/; + # re-register each .app's real nix-store target so `open -a`, Spotlight, + # Launchpad find them. home.activation.registerNixApps = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin (lib.hm.dag.entryAfter [ "linkGeneration" ] '' diff --git a/modules/home/darwin/window-manager/jankyborders.nix b/modules/home/darwin/window-manager/jankyborders.nix index 39f851b..3456600 100644 --- a/modules/home/darwin/window-manager/jankyborders.nix +++ b/modules/home/darwin/window-manager/jankyborders.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, utils, ... }: +{ config, lib, pkgs, helpers, ... }: # # JankyBorders — window borders (https://github.com/FelixKratz/JankyBorders) # @@ -16,8 +16,8 @@ in services.jankyborders = { enable = true; settings = { - active_color = utils.colors.toARGB config.theme.dark.blue 1; - inactive_color = utils.colors.toARGB config.theme.dark.blue 0; + active_color = helpers.colors.toARGB config.theme.dark.blue 1; + inactive_color = helpers.colors.toARGB config.theme.dark.blue 0; width = 8.0; }; }; diff --git a/modules/home/darwin/window-manager/sketchybar.nix b/modules/home/darwin/window-manager/sketchybar.nix index 74720d0..abfeda3 100644 --- a/modules/home/darwin/window-manager/sketchybar.nix +++ b/modules/home/darwin/window-manager/sketchybar.nix @@ -1,4 +1,4 @@ -{ pkgs, config, lib, utils, ... }: +{ pkgs, config, lib, helpers, ... }: # # SketchyBar — status bar for macOS, AeroSpace-integrated (Tokyo Night) # https://felixkratz.github.io/SketchyBar/setup @@ -43,7 +43,7 @@ in "sketchybar/colors.lua".text = let c = config.theme.dark; - toARGB = utils.colors.toARGB; + toARGB = helpers.colors.toARGB; in '' #!/usr/bin/env lua diff --git a/modules/home/default.nix b/modules/home/default.nix index 6c8df44..b245637 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -1,7 +1,12 @@ -{ ... }: +{ lib, hostSystem, ... }: # -# Shared home-manager modules (platform-generic). Opt-in per app via -# home.apps...enable. Darwin-only extras live in ./darwin/. +# Universal home-manager bundle. Every host imports via +# `imports = [ ../../modules/home ];`; the bundle self-selects the platform +# sub-bundle based on `hostSystem` (threaded from flake.nix extraSpecialArgs). +# +# `hostSystem` (not `pkgs.stdenv.hostPlatform.*`) because the imports list +# must not depend on `pkgs` — HM resolves pkgs via `_module.args` → `config`, +# so touching it during imports triggers infinite recursion. # { imports = [ @@ -9,5 +14,7 @@ ./fonts.nix ./shell.nix ./development - ]; + ] + ++ lib.optionals (lib.hasSuffix "-darwin" hostSystem) [ ./darwin ] + ++ lib.optionals (lib.hasSuffix "-linux" hostSystem) [ ./linux ]; } diff --git a/modules/home/development/claude-code.nix b/modules/home/development/claude-code.nix index 109648a..77b7f81 100644 --- a/modules/home/development/claude-code.nix +++ b/modules/home/development/claude-code.nix @@ -42,10 +42,10 @@ in enableMcpIntegration = true; # Bump with `nix flake update `. - plugins = [ - inputs.figma-plugin - "${inputs.claude-plugins-official}/plugins/typescript-lsp" - ]; + plugins = { + figma-plugin = inputs.figma-plugin; + typescript-lsp = "${inputs.claude-plugins-official}/plugins/typescript-lsp"; + }; # User-global MCP servers (project-scoped ones live in each project's .mcp.json). mcpServers = { diff --git a/modules/home/development/git.nix b/modules/home/development/git.nix index baec140..287fcfa 100644 --- a/modules/home/development/git.nix +++ b/modules/home/development/git.nix @@ -26,7 +26,13 @@ in ".emdash.json" ]; settings = { - credential.helper = "manager"; + credential = { + helper = "manager"; + } // lib.optionalAttrs pkgs.stdenv.isLinux { + # GCM needs an explicit credential store on Linux; gnome-keyring's + # Secret Service is already enabled system-wide. + credentialStore = "secretservice"; + }; }; includes = [ { diff --git a/modules/home/fonts.nix b/modules/home/fonts.nix index a0df378..2d2f29e 100644 --- a/modules/home/fonts.nix +++ b/modules/home/fonts.nix @@ -9,6 +9,9 @@ # https://www.nerdfonts.com/ home.packages = with pkgs; [ nerd-fonts.jetbrains-mono + dejavu_fonts + twemoji-color-font + noto-fonts-cjk-sans ]; # Enable fontconfig for proper font discovery and rendering across applications diff --git a/modules/home/lib.nix b/modules/home/lib.nix index 5bdfa6d..75137a6 100644 --- a/modules/home/lib.nix +++ b/modules/home/lib.nix @@ -1,4 +1,4 @@ -{ utils, ... }: +{ helpers, ... }: # # Home-manager library utilities # @@ -6,7 +6,7 @@ # These utilities are made available via the `config.lib` attribute. # # Available utilities: -# - lib.colors: Color manipulation functions (from utils.colors) +# - lib.colors: Color manipulation functions (from helpers.colors) # - toARGB: Convert hex color and alpha to ARGB format # # Usage in home-manager modules: @@ -17,6 +17,6 @@ # { config = { - lib.colors = utils.colors; + lib.colors = helpers.colors; }; } diff --git a/modules/home/linux/default.nix b/modules/home/linux/default.nix new file mode 100644 index 0000000..93b474e --- /dev/null +++ b/modules/home/linux/default.nix @@ -0,0 +1,13 @@ +{ ... }: +# +# Linux bundle — linux-only categories. Shared modules come via the +# universal bundle (see darwin/default.nix note on the removed back-ref). +# +{ + imports = [ + ./internet + ./media + ./thunar.nix + ./window-manager + ]; +} diff --git a/modules/home/linux/internet/default.nix b/modules/home/linux/internet/default.nix new file mode 100644 index 0000000..63ef1bf --- /dev/null +++ b/modules/home/linux/internet/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./librewolf.nix + ]; +} diff --git a/modules/home/linux/internet/librewolf.nix b/modules/home/linux/internet/librewolf.nix new file mode 100644 index 0000000..c3aa043 --- /dev/null +++ b/modules/home/linux/internet/librewolf.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: +# +# LibreWolf (https://librewolf.net) — hardened Firefox fork. +# +# Available options: +# - home.apps.internet.librewolf.enable +# +# `enhanced-h264ify` blocks AV1 on YouTube (Ice Lake has no HW AV1 decode) +# while leaving VP9 — which the Gen 11 media block DOES decode — available. +# Plain `h264ify` caps 4K/1440p (YouTube stopped H.264 encoding at those +# resolutions). +# +# extensions.packages symlinks profiles but does not enable them; the +# `autoDisableScopes = 0` pref is the standard HM idiom to auto-enable +# declaratively-installed extensions on first launch. +# +# NUR overlay wired at the system level (see hosts/astoria/default.nix). +# +let + cfg = config.home.apps.internet.librewolf; +in +{ + options.home.apps.internet.librewolf.enable = + lib.mkEnableOption "LibreWolf with enhanced-h264ify + nextcloud-passwords"; + + config = lib.mkIf cfg.enable { + programs.librewolf = { + enable = true; + profiles.default = { + settings = { + "extensions.autoDisableScopes" = 0; + "privacy.clearOnShutdown_v2.cookiesAndStorage" = false; + "privacy.clearOnShutdown_v2.cache" = false; + # resistFingerprinting forces prefers-color-scheme to "light" for + # all sites; fingerprintingProtection covers everything else RFP + # did except that, so sites see the real dark/light preference. + "privacy.resistFingerprinting" = false; + "privacy.fingerprintingProtection" = true; + "privacy.fingerprintingProtection.overrides" = "+AllTargets,-CSSPrefersColorScheme"; + "sidebar.revamp" = true; + "sidebar.verticalTabs" = true; + "sidebar.position_start" = false; + }; + + extensions.packages = with pkgs.nur.repos.rycee.firefox-addons; [ + enhanced-h264ify + nextcloud-passwords + ]; + }; + }; + }; +} diff --git a/modules/home/linux/media/default.nix b/modules/home/linux/media/default.nix new file mode 100644 index 0000000..847c371 --- /dev/null +++ b/modules/home/linux/media/default.nix @@ -0,0 +1,7 @@ +{ ... }: +{ + imports = [ + ./moonlight.nix + ./imv.nix + ]; +} diff --git a/modules/home/linux/media/imv.nix b/modules/home/linux/media/imv.nix new file mode 100644 index 0000000..75d4f9a --- /dev/null +++ b/modules/home/linux/media/imv.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: +# +# imv (https://sr.ht/~exec64/imv/) — lightweight Wayland-native image viewer. +# +# Available options: +# - home.apps.media.imv.enable +# +let + cfg = config.home.apps.media.imv; +in +{ + options.home.apps.media.imv.enable = + lib.mkEnableOption "imv image viewer"; + + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.imv ]; + + xdg.mimeApps.enable = true; + xdg.mimeApps.defaultApplications = { + "image/png" = [ "imv.desktop" ]; + "image/jpeg" = [ "imv.desktop" ]; + "image/gif" = [ "imv.desktop" ]; + "image/webp" = [ "imv.desktop" ]; + "image/bmp" = [ "imv.desktop" ]; + "image/tiff" = [ "imv.desktop" ]; + }; + }; +} diff --git a/modules/home/linux/media/moonlight.nix b/modules/home/linux/media/moonlight.nix new file mode 100644 index 0000000..6550f03 --- /dev/null +++ b/modules/home/linux/media/moonlight.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, ... }: +# +# Moonlight (https://moonlight-stream.org) — GameStream/Sunshine client. +# +# Available options: +# - home.apps.media.moonlight.enable +# +let + cfg = config.home.apps.media.moonlight; +in +{ + options.home.apps.media.moonlight.enable = + lib.mkEnableOption "Moonlight streaming client (moonlight-qt)"; + + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.moonlight-qt ]; + }; +} diff --git a/modules/home/linux/thunar.nix b/modules/home/linux/thunar.nix new file mode 100644 index 0000000..0b47bf7 --- /dev/null +++ b/modules/home/linux/thunar.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: +# +# Thunar (https://docs.xfce.org/xfce/thunar/start) — GTK file manager. +# +# Available options: +# - home.apps.filemanager.thunar.enable +# +# No upstream HM module; install packages only. GTK theming inherits from +# the host's gtk = { ... }; block. +# +let + cfg = config.home.apps.filemanager.thunar; +in +{ + options.home.apps.filemanager.thunar.enable = + lib.mkEnableOption "Thunar file manager with volume automount"; + + config = lib.mkIf cfg.enable { + home.packages = with pkgs; [ + thunar + thunar-volman + thunar-archive-plugin + ]; + }; +} diff --git a/modules/home/linux/window-manager/default.nix b/modules/home/linux/window-manager/default.nix new file mode 100644 index 0000000..10ae1a5 --- /dev/null +++ b/modules/home/linux/window-manager/default.nix @@ -0,0 +1,13 @@ +# +# Linux window manager stack: Sway + Waybar + Mako + Fuzzel + Swaylock. +# +{ + imports = [ + ./sway.nix + ./waybar.nix + ./mako.nix + ./fuzzel.nix + ./swaylock.nix + ./screenshot.nix + ]; +} diff --git a/modules/home/linux/window-manager/fuzzel.nix b/modules/home/linux/window-manager/fuzzel.nix new file mode 100644 index 0000000..a674157 --- /dev/null +++ b/modules/home/linux/window-manager/fuzzel.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: +# +# Fuzzel (https://codeberg.org/dnkl/fuzzel) — dmenu-like Wayland launcher. +# +# Available options: +# - home.apps.windowManager.fuzzel.enable +# +let + cfg = config.home.apps.windowManager.fuzzel; + c = config.theme.dark; + # fuzzel wants rrggbbaa hex without the leading '#'. + stripHash = s: lib.removePrefix "#" s; +in +{ + options.home.apps.windowManager.fuzzel.enable = + lib.mkEnableOption "Fuzzel application launcher"; + + config = lib.mkIf cfg.enable { + programs.fuzzel = { + enable = true; + settings = { + main = { + font = "JetBrainsMonoNL Nerd Font:size=12"; + terminal = "ghostty"; + layer = "overlay"; + width = 40; + lines = 15; + horizontal-pad = 20; + vertical-pad = 12; + inner-pad = 8; + }; + colors = { + background = "${stripHash c.bg}f0"; + text = "${stripHash c.fg}ff"; + match = "${stripHash c.blue}ff"; + selection = "${stripHash c.bg_highlight}ff"; + selection-text = "${stripHash c.fg}ff"; + selection-match = "${stripHash c.blue}ff"; + border = "${stripHash c.blue}ff"; + }; + border = { + width = 2; + radius = 6; + }; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/mako.nix b/modules/home/linux/window-manager/mako.nix new file mode 100644 index 0000000..6b237a6 --- /dev/null +++ b/modules/home/linux/window-manager/mako.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: +# +# Mako (https://github.com/emersion/mako) — Wayland notification daemon. +# +# Available options: +# - home.apps.windowManager.mako.enable +# +let + cfg = config.home.apps.windowManager.mako; + c = config.theme.dark; +in +{ + options.home.apps.windowManager.mako.enable = + lib.mkEnableOption "Mako notification daemon"; + + config = lib.mkIf cfg.enable { + services.mako = { + enable = true; + settings = { + font = "JetBrainsMonoNL Nerd Font 10"; + background-color = c.bg; + text-color = c.fg; + border-color = c.blue; + border-size = 2; + border-radius = 6; + default-timeout = 6000; + anchor = "top-right"; + margin = "8"; + padding = "10"; + max-visible = 5; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/screenshot.nix b/modules/home/linux/window-manager/screenshot.nix new file mode 100644 index 0000000..280d0cd --- /dev/null +++ b/modules/home/linux/window-manager/screenshot.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: +# +# Screenshots — grim/slurp/swappy; not spectacle/flameshot (don't target +# wlroots well). +# +# Available options: +# - home.apps.windowManager.screenshot.enable +# +let + cfg = config.home.apps.windowManager.screenshot; +in +{ + options.home.apps.windowManager.screenshot.enable = + lib.mkEnableOption "grim/slurp/swappy screenshot tooling"; + + config = lib.mkIf cfg.enable { + home.packages = [ + # Region select, opens swappy for annotate/save/copy. + (pkgs.writeShellScriptBin "screenshot-region" '' + set -euo pipefail + geometry="$(${pkgs.slurp}/bin/slurp)" || exit 0 + [ -n "$geometry" ] || exit 0 + exec ${pkgs.grim}/bin/grim -g "$geometry" - | ${pkgs.swappy}/bin/swappy -f - + '') + # Full screen, saved straight to disk + clipboard, no editor. + (pkgs.writeShellScriptBin "screenshot-full" '' + set -euo pipefail + dir="$HOME/Pictures/Screenshots" + mkdir -p "$dir" + f="$dir/screenshot-$(date +%Y%m%d-%H%M%S).png" + ${pkgs.grim}/bin/grim "$f" + ${pkgs.wl-clipboard}/bin/wl-copy < "$f" + ${pkgs.libnotify}/bin/notify-send "Screenshot saved" "$f" + '') + ]; + + xdg.configFile."swappy/config".text = '' + [Default] + save_dir=$HOME/Pictures/Screenshots + save_filename_format=screenshot-%Y%m%d-%H%M%S.png + show_panel=false + line_size=5 + text_size=20 + text_font=JetBrainsMonoNL Nerd Font + paint_mode=brush + early_exit=true + fill_shape=false + ''; + }; +} diff --git a/modules/home/linux/window-manager/sway.nix b/modules/home/linux/window-manager/sway.nix new file mode 100644 index 0000000..8705147 --- /dev/null +++ b/modules/home/linux/window-manager/sway.nix @@ -0,0 +1,184 @@ +{ config, lib, pkgs, ... }: +# +# Sway (https://swaywm.org) — wlroots-based tiling Wayland compositor. +# +# Available options: +# - home.apps.windowManager.sway.enable +# +# System-level `programs.sway.enable` in the host installs the compositor +# system-wide; this module owns user config (bindings, output, input, +# startup, idle chain, colors). +# +# grp:caps_toggle binds Capslock at the xkb layer to cycle us↔cz — no app +# can bind it and Capslock loses its latch function. +# +let + cfg = config.home.apps.windowManager.sway; +in +{ + options.home.apps.windowManager.sway.enable = + lib.mkEnableOption "Sway window manager (user-level config)"; + + config = lib.mkIf cfg.enable { + # Power-aware swayidle stages — swayidle itself has no AC/battery + # awareness, so each stage checks power state at fire time via + # /sys/class/power_supply/AC/online (this machine's actual AC device). + home.packages = [ + (pkgs.writeShellScriptBin "on-ac" '' + [ "$(cat /sys/class/power_supply/AC/online)" = "1" ] + '') + # 3min: dim on battery only (AC's dim is deferred to stage 2). + (pkgs.writeShellScriptBin "idle-stage1" '' + on-ac || brightnessctl -s set 20% + '') + # 5min: dim on AC, screen off on battery. + (pkgs.writeShellScriptBin "idle-stage2" '' + if on-ac; then + brightnessctl -s set 20% + else + swaymsg 'output * dpms off' + fi + '') + # 10min: screen off on AC, RAM-suspend on battery. + (pkgs.writeShellScriptBin "idle-stage3" '' + if on-ac; then + swaymsg 'output * dpms off' + else + systemctl suspend + fi + '') + # 20min: suspend-then-hibernate on AC (auto-hibernates ~30min later + # if left unplugged and unresumed), explicit hibernate on battery. + (pkgs.writeShellScriptBin "idle-stage4" '' + if on-ac; then + systemctl suspend-then-hibernate + else + systemctl hibernate + fi + '') + ]; + + home.pointerCursor = { + enable = true; + package = pkgs.capitaine-cursors; + name = "capitaine-cursors"; + size = 24; + gtk.enable = true; + x11.enable = true; + sway.enable = true; + }; + + wayland.windowManager.sway = { + enable = true; + wrapperFeatures.gtk = true; + + config = rec { + modifier = "Mod4"; + terminal = "ghostty"; + menu = "fuzzel"; + bars = [ ]; + + window.titlebar = false; + floating.titlebar = false; + gaps.smartBorders = "on"; + + output."eDP-1" = { + mode = "3840x2400@60Hz"; + scale = "2"; + }; + + input."type:keyboard" = { + xkb_layout = "us,cz"; + xkb_variant = "altgr-intl,"; + xkb_options = "grp:caps_toggle"; + }; + input."type:touchpad" = { + tap = "enabled"; + natural_scroll = "enabled"; + }; + + keybindings = let mod = modifier; in { + "${mod}+Return" = "exec ${terminal}"; + "${mod}+space" = "exec ${menu}"; + "${mod}+w" = "kill"; + "${mod}+Shift+e" = "exit"; + + "${mod}+h" = "focus left"; + "${mod}+j" = "focus down"; + "${mod}+k" = "focus up"; + "${mod}+l" = "focus right"; + + "${mod}+Ctrl+h" = "move left"; + "${mod}+Ctrl+j" = "move down"; + "${mod}+Ctrl+k" = "move up"; + "${mod}+Ctrl+l" = "move right"; + + "${mod}+1" = "workspace number 1"; + "${mod}+2" = "workspace number 2"; + # `pgrep -x moonlight` doesn't work here: the persistent process's comm + # is `.moonlight-wrap` (a truncated Nix wrapper name) + "${mod}+3" = ''exec "swaymsg -t get_tree | grep -q com.moonlight_stream.Moonlight || moonlight"; workspace number 3''; + "${mod}+4" = "workspace number 4"; + "${mod}+5" = "workspace number 5"; + + "${mod}+Ctrl+1" = "move container to workspace number 1"; + "${mod}+Ctrl+2" = "move container to workspace number 2"; + "${mod}+Ctrl+3" = "move container to workspace number 3"; + "${mod}+Ctrl+4" = "move container to workspace number 4"; + "${mod}+Ctrl+5" = "move container to workspace number 5"; + + "${mod}+e" = "layout toggle split"; + "${mod}+f" = "fullscreen"; + "${mod}+Shift+space" = "floating toggle"; + + "XF86MonBrightnessUp" = "exec brightnessctl set +5%"; + "XF86MonBrightnessDown" = "exec brightnessctl set 5%-"; + "XF86AudioRaiseVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"; + "XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; + "XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; + + "Print" = "exec screenshot-region"; + "Shift+Print" = "exec screenshot-full"; + }; + + startup = [ + { command = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; } + ]; + + assigns = { + "1" = [ { app_id = "librewolf"; } ]; + "2" = [ { app_id = "com.mitchellh.ghostty"; } ]; + "3" = [ { app_id = "com.moonlight_stream.Moonlight"; } ]; + }; + + window.commands = [ + { command = "focus"; criteria = { app_id = "librewolf"; }; } + { command = "focus"; criteria = { app_id = "com.mitchellh.ghostty"; }; } + { command = "focus"; criteria = { app_id = "com.moonlight_stream.Moonlight"; }; } + ]; + + colors = let c = config.theme.dark; in { + focused = { border = c.blue; background = c.bg; text = c.fg; indicator = c.blue; childBorder = c.blue; }; + focusedInactive = { border = c.bg_dark; background = c.bg; text = c.fg_dark; indicator = c.bg_dark; childBorder = c.bg_dark; }; + unfocused = { border = c.bg_dark; background = c.bg; text = c.comment; indicator = c.bg_dark; childBorder = c.bg_dark; }; + urgent = { border = c.red; background = c.red; text = c.fg; indicator = c.red; childBorder = c.red; }; + }; + + fonts = { + names = [ "JetBrainsMonoNL Nerd Font" ]; + size = 10.0; + }; + }; + + extraConfig = '' + seat seat0 xcursor_theme capitaine-cursors 24 + + exec swayidle -w \ + timeout 180 idle-stage1 resume 'brightnessctl -r' \ + timeout 300 idle-stage2 resume 'swaymsg "output * dpms on"; brightnessctl -r' \ + timeout 600 idle-stage3 resume 'swaymsg "output * dpms on"' \ + timeout 1200 idle-stage4 + ''; + }; + }; +} diff --git a/modules/home/linux/window-manager/swaylock.nix b/modules/home/linux/window-manager/swaylock.nix new file mode 100644 index 0000000..b3d34f7 --- /dev/null +++ b/modules/home/linux/window-manager/swaylock.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: +# +# Swaylock (https://github.com/swaywm/swaylock) — Sway screen locker. +# +# Available options: +# - home.apps.windowManager.swaylock.enable +# +# Invoked by swayidle timeout chain (see sway.nix) and Mod+Escape. +# +let + cfg = config.home.apps.windowManager.swaylock; + c = config.theme.dark; + # swaylock reads rrggbb hex, no leading '#'. + stripHash = s: lib.removePrefix "#" s; +in +{ + options.home.apps.windowManager.swaylock.enable = + lib.mkEnableOption "Swaylock screen locker"; + + config = lib.mkIf cfg.enable { + programs.swaylock = { + enable = true; + settings = { + color = stripHash c.bg; + font = "JetBrainsMonoNL Nerd Font"; + font-size = 24; + + indicator-radius = 100; + indicator-thickness = 10; + + ring-color = stripHash c.bg_dark; + ring-clear-color = stripHash c.orange; + ring-caps-lock-color = stripHash c.yellow; + ring-ver-color = stripHash c.blue; + ring-wrong-color = stripHash c.red; + + key-hl-color = stripHash c.blue; + bs-hl-color = stripHash c.red; + text-color = stripHash c.fg; + text-clear-color = stripHash c.orange; + text-ver-color = stripHash c.blue; + text-wrong-color = stripHash c.red; + text-caps-lock-color = stripHash c.yellow; + + inside-color = stripHash c.bg; + inside-clear-color = stripHash c.bg; + inside-ver-color = stripHash c.bg; + inside-wrong-color = stripHash c.bg; + inside-caps-lock-color = stripHash c.bg; + + line-color = stripHash c.bg; + + show-failed-attempts = true; + daemonize = true; + ignore-empty-password = true; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/waybar.nix b/modules/home/linux/window-manager/waybar.nix new file mode 100644 index 0000000..02565f9 --- /dev/null +++ b/modules/home/linux/window-manager/waybar.nix @@ -0,0 +1,132 @@ +{ config, lib, pkgs, ... }: +# +# Waybar (https://github.com/Alexays/Waybar) — Sway status bar. +# +# Available options: +# - home.apps.windowManager.waybar.enable +# +# The `tray` module is required for blueman-applet (StatusNotifierItem); +# without it the applet runs invisibly. +# +let + cfg = config.home.apps.windowManager.waybar; + c = config.theme.dark; +in +{ + options.home.apps.windowManager.waybar.enable = + lib.mkEnableOption "Waybar status bar for Sway"; + + config = lib.mkIf cfg.enable { + programs.waybar = { + enable = true; + systemd.enable = true; + + settings.mainBar = { + layer = "top"; + position = "top"; + height = 28; + spacing = 6; + modules-left = [ "sway/workspaces" "sway/mode" ]; + modules-center = [ "sway/window" ]; + modules-right = [ "tray" "sway/language" "pulseaudio" "network" "battery" "clock" ]; + + "sway/workspaces" = { + disable-scroll = true; + all-outputs = true; + persistent-workspaces = { + "1" = [ ]; + "2" = [ ]; + "3" = [ ]; + "4" = [ ]; + "5" = [ ]; + }; + }; + + "sway/language" = { + format = "󰌌 {short}"; + }; + + tray = { spacing = 10; }; + + clock = { + format = " {:%a %d %b %H:%M}"; + tooltip-format = "{:%Y-%m-%d}\n{calendar}"; + }; + + battery = { + format = "{icon} {capacity}%"; + format-icons = [ "󰂎" "󰁻" "󰁾" "󰂀" "󰂂" ]; + format-charging = "󰂄 {capacity}%"; + states = { warning = 30; critical = 15; }; + }; + + network = { + format-wifi = "󰤨 {essid} ({signalStrength}%)"; + format-ethernet = "󰈀 {ifname}"; + format-disconnected = "󰌙 disconnected"; + tooltip-format = "{ifname}: {ipaddr}/{cidr}"; + on-click = "${pkgs.ghostty}/bin/ghostty --command=nmtui"; + }; + + pulseaudio = { + format = "{icon} {volume}%"; + format-muted = "󰖁 muted"; + format-icons = { default = [ "󰕿" "󰖀" "󰕾" ]; }; + on-click = "${pkgs.pavucontrol}/bin/pavucontrol"; + }; + }; + + style = '' + * { + font-family: "JetBrainsMonoNL Nerd Font", monospace; + font-size: 12px; + border: none; + border-radius: 0; + min-height: 0; + } + + window#waybar { + background: ${c.bg}; + color: ${c.fg}; + } + + #workspaces { + margin: 4px 6px; + } + #workspaces button { + padding: 0; + margin: 2px 4px; + font-weight: bold; + color: alpha(${c.fg}, 0.53); + background: alpha(${c.blue}, 0.2); + border-radius: 6px; + } + #workspaces button.empty { + color: alpha(${c.fg}, 0.3); + background: ${c.bg_dark}; + } + #workspaces button.focused { + color: ${c.black}; + background: ${c.purple}; + } + #workspaces button.urgent { + color: ${c.fg}; + background: ${c.red}; + } + + #clock, #battery, #network, #pulseaudio, #tray, #mode, #language { + padding: 0 6px; + color: ${c.fg}; + } + + #network:hover, #pulseaudio:hover { + background: ${c.bg_highlight}; + border-radius: 6px; + } + + #battery.warning { color: ${c.orange}; } + #battery.critical { color: ${c.red}; } + ''; + }; + }; +} diff --git a/modules/lib/darwin-hosts.nix b/modules/lib/darwin-hosts.nix new file mode 100644 index 0000000..190a4f6 --- /dev/null +++ b/modules/lib/darwin-hosts.nix @@ -0,0 +1,159 @@ +{ lib }: +# +# Darwin host-wiring helpers +# +# Functions that assemble nix-darwin module lists and pkgs instantiations for +# a host's `darwinConfigurations.` block in flake.nix. Unlike +# modules/lib/default.nix (whose `colors`/`darwin` helpers are threaded into +# module bodies via the `helpers` specialArg), these functions are consumed +# ONLY by flake.nix itself, before any specialArgs wiring happens. +# +# Because they build module lists and pkgs sets rather than being pure config +# utilities, every value they'd otherwise close over (home-manager, +# nix-homebrew, nixpkgs, inputs, rootDir, helpers) is passed in explicitly by +# the caller in flake.nix — including `helpers` itself, since this file +# cannot reference the `helpers` binding it is a sibling of. +# +{ + # Build the home-manager module list for a darwin host + # + # Integrates home-manager with the system configuration and imports + # user-specific settings from hosts/${hostname}/home.nix. + # + # Arguments: + # home-manager - The home-manager flake input (darwinModules.home-manager) + # inputs - The flake's full `inputs` attrset, threaded into extraSpecialArgs + # rootDir - The flake's `self`, threaded into extraSpecialArgs and used + # to build an absolute path to hosts/${hostname}/home.nix + # (a relative `./hosts/...` path here would resolve against + # this file's own directory, not the flake root) + # helpers - The `helpers` value from modules/lib/default.nix, threaded + # into extraSpecialArgs + # username - The user account this home-manager config applies to + # hostname - Host directory name under hosts/, used to locate home.nix + # home - Absolute path to the user's home directory + # system - Nix system string (e.g. "aarch64-darwin"), threaded as hostSystem + # private - Per-host private values (default null) + # + # Returns: + # A list of modules to append to a darwinSystem's `modules` + # + # Example: + # darwinHosts.mkHomeConfig { + # inherit home-manager inputs rootDir helpers; + # username = "pavla"; hostname = "makima"; system = "aarch64-darwin"; + # home = "/Users/pavla"; + # } + # + mkHomeConfig = { home-manager, inputs, rootDir, helpers, username, hostname, home, system, private ? null }: [ + home-manager.darwinModules.home-manager + { + # Set the user's home directory path + users.users.${username}.home = lib.mkDefault home; + + # Use the system's nixpkgs instance for home-manager + home-manager.useGlobalPkgs = true; + # Install user packages to /etc/profiles instead of ~/.nix-profile. + # `private` is always present (null on hosts without one) so modules + # can pattern-match on it without triggering _module.args recursion. + # + # `hostSystem` is the Nix system string ("aarch64-darwin", + # "x86_64-linux", ...). Threaded via specialArgs so modules/home + # can decide platform sub-bundle imports (see modules/home/default.nix) + # without depending on `pkgs.stdenv.hostPlatform.*` at import-list + # eval time — that route hits `_module.args`->`config` recursion + # because pkgs isn't externally provided to HM's inner modules. + home-manager.extraSpecialArgs = { + inherit inputs private helpers; + hostSystem = system; + rootDir = rootDir; + }; + + home-manager.useUserPackages = true; + + # When a file home-manager wants to manage already exists (e.g. + # KeePassXC writes its own keepassxc.ini before we declare it), + # move the existing file to `.backup` instead of aborting. + home-manager.backupFileExtension = "backup"; + + home-manager.users.${username} = { + home.username = username; + + imports = [ + (rootDir + "/hosts/${hostname}/home.nix") + ]; + }; + } + ]; + + # Build the nix-homebrew module list for a darwin host + # + # Creates a module list that integrates nix-homebrew with the system + # configuration. nix-homebrew manages Homebrew installation itself, while + # nix-darwin's homebrew module manages packages declaratively. + # + # Arguments: + # nix-homebrew - The nix-homebrew flake input (darwinModules.nix-homebrew) + # username - The user who owns the Homebrew installation + # taps - Optional attrset of Homebrew taps to manage declaratively + # autoMigrate - Whether to automatically migrate an existing Homebrew + # installation + # + # Returns: + # A list of modules to append to a darwinSystem's `modules` + # + # Example: + # darwinHosts.mkHomebrewConfig { inherit nix-homebrew; username = "pavla"; } + # + mkHomebrewConfig = { nix-homebrew, username, taps ? { }, autoMigrate ? true }: [ + nix-homebrew.darwinModules.nix-homebrew + { + # Set the primary user for nix-darwin + system.primaryUser = username; + + nix-homebrew = { + enable = true; + enableRosetta = true; + user = username; + taps = taps; + mutableTaps = true; + autoMigrate = autoMigrate; + }; + } + ]; + + # Build the pkgs instantiation for a darwin host + # + # Wraps `import nixpkgs { ... }` with the overlays every darwin host needs: + # a fish `doCheck = false` override (fish's test suite doesn't pass in our + # darwin build environment) and our own ./pkgs overlay. + # + # Arguments: + # nixpkgs - The nixpkgs flake input + # system - Nix system string (e.g. "aarch64-darwin") + # helpers - The `helpers` value from modules/lib/default.nix, passed + # through to the ./pkgs overlay + # rootDir - The flake's `self`, used to build an absolute path to + # ./pkgs (a relative path here would resolve against this + # file's own directory, not the flake root) + # allowUnfree - Whether to allow unfree packages (default true) + # overlays - Extra host-specific overlays, applied after the shared ones + # + # Returns: + # An instantiated nixpkgs set + # + # Example: + # darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; } + # + mkDarwinPkgs = { nixpkgs, system, helpers, rootDir, allowUnfree ? true, overlays ? [ ] }: + import nixpkgs { + inherit system; + config = { inherit allowUnfree; }; + overlays = [ + (final: prev: { + fish = prev.fish.overrideAttrs (old: { doCheck = false; }); + }) + (import (rootDir + "/pkgs") { inherit helpers; }) + ] ++ overlays; + }; +} diff --git a/modules/lib/default.nix b/modules/lib/default.nix index b6dca6b..98e01fa 100644 --- a/modules/lib/default.nix +++ b/modules/lib/default.nix @@ -2,7 +2,7 @@ # # Custom utility functions # -# Global utilities available across the entire configuration via the `utils` special arg. +# Global utilities available across the entire configuration via the `helpers` special arg. # Imported in flake.nix and passed to all modules via specialArgs. # # These utilities are system-wide and available to both nix-darwin and home-manager modules. @@ -56,7 +56,7 @@ # A derivation that installs the macOS application # # Example: - # utils.darwin.mkDmgApp { + # helpers.darwin.mkDmgApp { # inherit stdenv fetchurl; # pname = "insync"; # version = "3.8.7.50505"; @@ -103,7 +103,7 @@ # A marker derivation with the cask name in passthru # # Example: - # utils.darwin.mkBrewCask { caskName = "karabiner-elements"; } + # helpers.darwin.mkBrewCask { caskName = "karabiner-elements"; } # # Usage in modules: # When a package created with mkBrewCask is added to environment.systemPackages, diff --git a/pkgs/affinity/default.nix b/pkgs/affinity/default.nix index e9e1f97..0ef9c45 100644 --- a/pkgs/affinity/default.nix +++ b/pkgs/affinity/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Affinity # for macOS # # Image editing and design software for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "affinity"; } +helpers.darwin.mkBrewCask { caskName = "affinity"; } diff --git a/pkgs/alt-tab-macos/default.nix b/pkgs/alt-tab-macos/default.nix index 1809c47..a49fcf7 100644 --- a/pkgs/alt-tab-macos/default.nix +++ b/pkgs/alt-tab-macos/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Alt-Tab for macOS # # Windows-style alt-tab window switcher for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "alt-tab"; } +helpers.darwin.mkBrewCask { caskName = "alt-tab"; } diff --git a/pkgs/android-studio/default.nix b/pkgs/android-studio/default.nix index ef1a1f5..0787b6e 100644 --- a/pkgs/android-studio/default.nix +++ b/pkgs/android-studio/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Android Studio for macOS # @@ -9,4 +9,4 @@ # Android Studio is darwin-only in this overlay; the SDK Manager still runs # inside the app to fetch platform-tools/emulator/system-images at first run. # -utils.darwin.mkBrewCask { caskName = "android-studio"; } +helpers.darwin.mkBrewCask { caskName = "android-studio"; } diff --git a/pkgs/better-touch-tool/default.nix b/pkgs/better-touch-tool/default.nix index 4490ebb..b1c2f94 100644 --- a/pkgs/better-touch-tool/default.nix +++ b/pkgs/better-touch-tool/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # BetterTouchTool for macOS # # Custom gesture and touchpad configuration tool for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "bettertouchtool"; } \ No newline at end of file +helpers.darwin.mkBrewCask { caskName = "bettertouchtool"; } \ No newline at end of file diff --git a/pkgs/blurred/default.nix b/pkgs/blurred/default.nix index a4ee229..a03c5f9 100644 --- a/pkgs/blurred/default.nix +++ b/pkgs/blurred/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Blurred for macOS # # Utility to dim background/inactive windows and content. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "blurred"; } +helpers.darwin.mkBrewCask { caskName = "blurred"; } diff --git a/pkgs/default.nix b/pkgs/default.nix index e954993..40222a3 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -20,56 +20,60 @@ # - obs-studio: Screen recorder/streamer (brew cask on macOS — nixpkgs is Linux-only) # - keepassxc: Password manager (brew cask on macOS — sidesteps qtmacextras # linker crash; Linux uses nixpkgs) +# - tokyonight-gtk-theme: GTK theme (nixpkgs dropped it over a dead GTK2 +# dependency the theme doesn't need — see pkgs/tokyonight-gtk-theme) # # This overlay is applied in flake.nix when creating the pkgs instance. # -{ utils }: +{ helpers }: final: prev: { keepassxc = if prev.stdenv.isDarwin - then prev.callPackage ./keepassxc { inherit utils; } + then prev.callPackage ./keepassxc { inherit helpers; } else prev.keepassxc; insync = if prev.stdenv.isDarwin - then prev.callPackage ./insync { inherit utils; } + then prev.callPackage ./insync { inherit helpers; } else prev.insync; - blurred = prev.callPackage ./blurred { inherit utils; }; + blurred = prev.callPackage ./blurred { inherit helpers; }; - docker-desktop = prev.callPackage ./docker-desktop { inherit utils; }; + docker-desktop = prev.callPackage ./docker-desktop { inherit helpers; }; - orbstack = prev.callPackage ./orbstack { inherit utils; }; + orbstack = prev.callPackage ./orbstack { inherit helpers; }; ungoogled-chromium = if prev.stdenv.isDarwin - then prev.callPackage ./ungoogled-chromium { inherit utils; } + then prev.callPackage ./ungoogled-chromium { inherit helpers; } else prev.ungoogled-chromium; - better-touch-tool = prev.callPackage ./better-touch-tool { inherit utils; }; + better-touch-tool = prev.callPackage ./better-touch-tool { inherit helpers; }; raycast = if prev.stdenv.isDarwin - then prev.callPackage ./raycast { inherit utils; } + then prev.callPackage ./raycast { inherit helpers; } else prev.raycast; android-studio = if prev.stdenv.isDarwin - then prev.callPackage ./android-studio { inherit utils; } + then prev.callPackage ./android-studio { inherit helpers; } else prev.android-studio; - emdash = prev.callPackage ./emdash { inherit utils; }; + emdash = prev.callPackage ./emdash { inherit helpers; }; zed-editor = if prev.stdenv.isDarwin - then prev.callPackage ./zed-editor { inherit utils; } + then prev.callPackage ./zed-editor { inherit helpers; } else prev.zed-editor; - microsoft-teams = prev.callPackage ./microsoft-teams { inherit utils; }; + microsoft-teams = prev.callPackage ./microsoft-teams { inherit helpers; }; - microsoft-outlook = prev.callPackage ./microsoft-outlook { inherit utils; }; + microsoft-outlook = prev.callPackage ./microsoft-outlook { inherit helpers; }; - affinity = prev.callPackage ./affinity { inherit utils; }; + affinity = prev.callPackage ./affinity { inherit helpers; }; - obs-studio = prev.callPackage ./obs-studio { inherit utils; }; + obs-studio = prev.callPackage ./obs-studio { inherit helpers; }; + + tokyonight-gtk-theme = prev.callPackage ./tokyonight-gtk-theme { }; } diff --git a/pkgs/docker-desktop/default.nix b/pkgs/docker-desktop/default.nix index ee8e4e8..9a0b5e8 100644 --- a/pkgs/docker-desktop/default.nix +++ b/pkgs/docker-desktop/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Docker Desktop for macOS # # Complete Docker environment with GUI and daemon. # Installed via Homebrew cask for easier updates. VM configuration managed by Docker Desktop. # -utils.darwin.mkBrewCask { caskName = "docker-desktop"; } +helpers.darwin.mkBrewCask { caskName = "docker-desktop"; } diff --git a/pkgs/emdash/default.nix b/pkgs/emdash/default.nix index 32d858e..fbac50d 100644 --- a/pkgs/emdash/default.nix +++ b/pkgs/emdash/default.nix @@ -1,4 +1,4 @@ -{ utils, lib, stdenv, fetchurl, appimageTools, makeWrapper }: +{ helpers, lib, stdenv, fetchurl, appimageTools, makeWrapper }: # # Emdash — multi-agent dev environment (https://emdash.sh) # @@ -15,7 +15,7 @@ # https://github.com/generalaction/emdash/releases/download/v/emdash-x86_64.AppImage # let - cfg = utils.darwin; + cfg = helpers.darwin; in if stdenv.hostPlatform.isDarwin then cfg.mkBrewCask { caskName = "emdash"; } diff --git a/pkgs/insync/default.nix b/pkgs/insync/default.nix index 1aab261..93ba8a6 100644 --- a/pkgs/insync/default.nix +++ b/pkgs/insync/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Insync for macOS # @@ -6,4 +6,4 @@ # Installed via Homebrew cask on macOS for easier updates. # Linux uses the nixpkgs version (handled by overlay in pkgs/default.nix). # -utils.darwin.mkBrewCask { caskName = "insync"; } +helpers.darwin.mkBrewCask { caskName = "insync"; } diff --git a/pkgs/karabiner-elements/default.nix b/pkgs/karabiner-elements/default.nix index 6a0b6ab..571829b 100644 --- a/pkgs/karabiner-elements/default.nix +++ b/pkgs/karabiner-elements/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Karabiner-Elements for macOS # # Keyboard customizer that allows complex key remapping and shortcuts. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "karabiner-elements"; } +helpers.darwin.mkBrewCask { caskName = "karabiner-elements"; } diff --git a/pkgs/keepassxc/default.nix b/pkgs/keepassxc/default.nix index 693f9bc..376a856 100644 --- a/pkgs/keepassxc/default.nix +++ b/pkgs/keepassxc/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # KeePassXC — https://keepassxc.org # @@ -9,7 +9,7 @@ # /Applications/... prefix. Reference as `pkgs.keepassxc.passthru.cli`. # let - marker = utils.darwin.mkBrewCask { caskName = "keepassxc"; }; + marker = helpers.darwin.mkBrewCask { caskName = "keepassxc"; }; appMacOS = "/Applications/KeePassXC.app/Contents/MacOS"; in marker // { diff --git a/pkgs/microsoft-outlook/default.nix b/pkgs/microsoft-outlook/default.nix index 09b8d82..0da30e4 100644 --- a/pkgs/microsoft-outlook/default.nix +++ b/pkgs/microsoft-outlook/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Microsoft Outlook for macOS # @@ -6,4 +6,4 @@ # Exchange accounts. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "microsoft-outlook"; } +helpers.darwin.mkBrewCask { caskName = "microsoft-outlook"; } diff --git a/pkgs/microsoft-teams/default.nix b/pkgs/microsoft-teams/default.nix index 429df46..73d2b4e 100644 --- a/pkgs/microsoft-teams/default.nix +++ b/pkgs/microsoft-teams/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Microsoft Teams for macOS # @@ -6,4 +6,4 @@ # used for corporate/work communication. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "microsoft-teams"; } +helpers.darwin.mkBrewCask { caskName = "microsoft-teams"; } diff --git a/pkgs/obs-studio/default.nix b/pkgs/obs-studio/default.nix index 27df3ca..4f5cd4b 100644 --- a/pkgs/obs-studio/default.nix +++ b/pkgs/obs-studio/default.nix @@ -1,4 +1,4 @@ -{ utils, stdenv, obs-studio }: +{ helpers, stdenv, obs-studio }: # # OBS Studio — https://obsproject.com # @@ -7,6 +7,6 @@ # System Settings → Privacy & Security after first "Start Virtual Camera". # if stdenv.hostPlatform.isDarwin then - utils.darwin.mkBrewCask { caskName = "obs"; } + helpers.darwin.mkBrewCask { caskName = "obs"; } else obs-studio diff --git a/pkgs/orbstack/default.nix b/pkgs/orbstack/default.nix index 6c73424..9dde1f8 100644 --- a/pkgs/orbstack/default.nix +++ b/pkgs/orbstack/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # OrbStack for macOS # # Fast, lightweight Docker and Linux VM alternative to Docker Desktop. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "orbstack"; } +helpers.darwin.mkBrewCask { caskName = "orbstack"; } diff --git a/pkgs/raycast/default.nix b/pkgs/raycast/default.nix index 4c7ec4a..60fb9b3 100644 --- a/pkgs/raycast/default.nix +++ b/pkgs/raycast/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Raycast for macOS # @@ -12,4 +12,4 @@ # # Raycast is darwin-only (no Linux build); the overlay leaves Linux alone. # -utils.darwin.mkBrewCask { caskName = "raycast"; } +helpers.darwin.mkBrewCask { caskName = "raycast"; } diff --git a/pkgs/tokyonight-gtk-theme/default.nix b/pkgs/tokyonight-gtk-theme/default.nix new file mode 100644 index 0000000..19dfdb1 --- /dev/null +++ b/pkgs/tokyonight-gtk-theme/default.nix @@ -0,0 +1,125 @@ +{ + lib, + stdenvNoCC, + fetchFromGitHub, + gnome-shell, + sassc, + gnome-themes-extra, + colorVariants ? [ ], + sizeVariants ? [ ], + themeVariants ? [ ], + tweakVariants ? [ ], + iconVariants ? [ ], +}: +# +# Vendored copy of nixpkgs's pkgs/by-name/to/tokyonight-gtk-theme — +# nixpkgs removed the package because its propagatedUserEnvPkgs referenced +# gtk-engine-murrine, itself removed for depending on GTK2. The theme's +# CSS-based rendering never actually needed that engine, so this drops the +# gtk-engine-murrine arg/propagatedUserEnvPkgs line and keeps everything +# else identical. Delete this and go back to +# `pkgs.tokyonight-gtk-theme.override {...}` once nixpkgs restores it. +# +let + pname = "tokyonight-gtk-theme"; + colorVariantList = [ + "dark" + "light" + ]; + sizeVariantList = [ + "compact" + "standard" + ]; + themeVariantList = [ + "default" + "green" + "grey" + "orange" + "pink" + "purple" + "red" + "teal" + "yellow" + "all" + ]; + tweakVariantList = [ + "moon" + "storm" + "black" + "float" + "outline" + "macos" + ]; + iconVariantList = [ + "Dark-Cyan" + "Dark" + "Light" + "Moon" + ]; +in +lib.checkListOfEnum "${pname}: colorVariants" colorVariantList colorVariants lib.checkListOfEnum + "${pname}: sizeVariants" + sizeVariantList + sizeVariants + lib.checkListOfEnum + "${pname}: themeVariants" + themeVariantList + themeVariants + lib.checkListOfEnum + "${pname}: tweakVariants" + tweakVariantList + tweakVariants + lib.checkListOfEnum + "${pname}: iconVariants" + iconVariantList + iconVariants + + stdenvNoCC.mkDerivation + { + inherit pname; + version = "0-unstable-2025-10-23"; + + src = fetchFromGitHub { + owner = "Fausto-Korpsvart"; + repo = "Tokyonight-GTK-Theme"; + rev = "6c340e058e84c1975a038a8e5d1e384477225dc0"; + hash = "sha256-7H2n9wTaW8Db1RejWK071ITV1j5KIuzfql0Tx9WT6zM="; + }; + + nativeBuildInputs = [ + gnome-shell + sassc + ]; + buildInputs = [ gnome-themes-extra ]; + + dontBuild = true; + + postPatch = '' + patchShebangs themes/install.sh + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/share/themes + cd themes + ./install.sh -n Tokyonight \ + ${lib.optionalString (colorVariants != [ ]) "-c " + toString colorVariants} \ + ${lib.optionalString (sizeVariants != [ ]) "-s " + toString sizeVariants} \ + ${lib.optionalString (themeVariants != [ ]) "-t " + toString themeVariants} \ + ${lib.optionalString (tweakVariants != [ ]) "--tweaks " + toString tweakVariants} \ + -d "$out/share/themes" + cd ../icons + ${lib.optionalString (iconVariants != [ ]) '' + mkdir -p $out/share/icons + cp -a ${toString (map (v: "Tokyonight-${v}") iconVariants)} $out/share/icons/ + ''} + runHook postInstall + ''; + + meta = { + description = "GTK theme based on the Tokyo Night colour palette"; + homepage = "https://github.com/Fausto-Korpsvart/Tokyonight-GTK-Theme"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.unix; + }; + } diff --git a/pkgs/ungoogled-chromium/default.nix b/pkgs/ungoogled-chromium/default.nix index d978f17..9fbf82d 100644 --- a/pkgs/ungoogled-chromium/default.nix +++ b/pkgs/ungoogled-chromium/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Ungoogled Chromium for macOS # @@ -6,4 +6,4 @@ # Installed via Homebrew cask on macOS for easier updates. # Linux uses the nixpkgs version (handled by overlay in pkgs/default.nix). # -utils.darwin.mkBrewCask { caskName = "ungoogled-chromium"; } +helpers.darwin.mkBrewCask { caskName = "ungoogled-chromium"; } diff --git a/pkgs/zed-editor/default.nix b/pkgs/zed-editor/default.nix index 47eb2bf..0170ad8 100644 --- a/pkgs/zed-editor/default.nix +++ b/pkgs/zed-editor/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Zed editor for macOS # @@ -13,4 +13,4 @@ # nixpkgs source : https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/ze/zed-editor # upstream cache tracking issue: https://github.com/zed-industries/zed/issues/26277 # -utils.darwin.mkBrewCask { caskName = "zed"; } +helpers.darwin.mkBrewCask { caskName = "zed"; }