NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

commit 3fb6fc67c642c628b17a2944f617bad6851504a3
parent 0d80e3262c9f6b0ed4e5d7d1659358576de413df
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Thu,  8 Oct 2026 06:58:51 +0100

added niri and noctalia i now have the option to choose between hypr and celestia and niri and noc

Diffstat:
M.gitignore | 4++++
MREADME.md | 169++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Adocs/superpowers/plans/2026-10-08-dendritic-niri-noctalia.md | 375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/superpowers/specs/2026-10-08-dendritic-niri-noctalia-design.md | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mflake.lock | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mflake.nix | 82++++++++++++++++++++++++-------------------------------------------------------
Dhome/cheats/gpg.md | 321-------------------------------------------------------------------------------
Dhome/cheats/nix.md | 238-------------------------------------------------------------------------------
Dhome/default.nix | 32--------------------------------
Dhome/modules/caelestia.nix | 249-------------------------------------------------------------------------------
Dhome/modules/cheats.nix | 61-------------------------------------------------------------
Dhome/modules/dotfiles.nix | 114-------------------------------------------------------------------------------
Dhome/modules/fan.nix | 120-------------------------------------------------------------------------------
Dhome/modules/git.nix | 81-------------------------------------------------------------------------------
Dhome/modules/gpg.nix | 51---------------------------------------------------
Dhome/modules/gtk.nix | 26--------------------------
Dhome/modules/hyprland.nix | 149-------------------------------------------------------------------------------
Dhome/modules/media.nix | 115-------------------------------------------------------------------------------
Dhome/modules/neovim.nix | 115-------------------------------------------------------------------------------
Dhome/modules/prompt.nix | 245-------------------------------------------------------------------------------
Dhome/modules/shell.nix | 115-------------------------------------------------------------------------------
Dhome/modules/sops.nix | 43-------------------------------------------
Dhome/modules/ssh.nix | 46----------------------------------------------
Dhome/modules/terminal.nix | 159-------------------------------------------------------------------------------
Dhome/modules/tools.nix | 116-------------------------------------------------------------------------------
Dhome/modules/yazi.nix | 130-------------------------------------------------------------------------------
Dhosts/bootstrap/default.nix | 89-------------------------------------------------------------------------------
Dhosts/laptop/default.nix | 142-------------------------------------------------------------------------------
Dhosts/laptop/fan-cli.nix | 122-------------------------------------------------------------------------------
Dhosts/laptop/fan-extras.nix | 25-------------------------
Dhosts/laptop/fan-throttle-guard.nix | 122-------------------------------------------------------------------------------
Dhosts/laptop/hardware-configuration.nix | 45---------------------------------------------
Dhosts/laptop/nix-settings.nix | 41-----------------------------------------
Dhosts/laptop/nvidia.nix | 53-----------------------------------------------------
Dhosts/laptop/sops.nix | 32--------------------------------
Dhosts/laptop/ssd.nix | 30------------------------------
Dhosts/laptop/toolbox.nix | 26--------------------------
Dhosts/laptop/uniwill-laptop.nix | 20--------------------
Amodules/features/desktop/hyprland.nix | 24++++++++++++++++++++++++
Amodules/features/desktop/niri.nix | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/features/desktop/noctalia.nix | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/features/desktop/options.nix | 29+++++++++++++++++++++++++++++
Amodules/features/home-manager.nix | 27+++++++++++++++++++++++++++
Amodules/features/workstation.nix | 48++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/caelestia.nix | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/caelestia/active-window-program-name.patch -> modules/home/caelestia/active-window-program-name.patch | 0
Rhome/caelestia/rose-pine-dark.txt -> modules/home/caelestia/rose-pine-dark.txt | 0
Rhome/caelestia/rose-pine-dawn.txt -> modules/home/caelestia/rose-pine-dawn.txt | 0
Rhome/caelestia/scheme-dawn.json -> modules/home/caelestia/scheme-dawn.json | 0
Rhome/caelestia/scheme.json -> modules/home/caelestia/scheme.json | 0
Rhome/caelestia/shell-tokens.json -> modules/home/caelestia/shell-tokens.json | 0
Amodules/home/cheats.nix | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/cheats/gpg.md | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/cheats/niri.md | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/cheats/nix.md | 266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/default.nix | 41+++++++++++++++++++++++++++++++++++++++++
Amodules/home/dotfiles.nix | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/fan.nix | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/git.nix | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/gpg.nix | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/gpg/daemon-main.pub.asc -> modules/home/gpg/daemon-main.pub.asc | 0
Amodules/home/gtk.nix | 31+++++++++++++++++++++++++++++++
Rhome/hypr/bindings.lua -> modules/home/hypr/bindings.lua | 0
Rhome/hypr/caelestia.lua -> modules/home/hypr/caelestia.lua | 0
Rhome/hypr/core.lua -> modules/home/hypr/core.lua | 0
Rhome/hypr/defaults.lua -> modules/home/hypr/defaults.lua | 0
Rhome/hypr/lid.lua -> modules/home/hypr/lid.lua | 0
Rhome/hypr/looknfeel.lua -> modules/home/hypr/looknfeel.lua | 0
Rhome/hypr/omarchy.lua -> modules/home/hypr/omarchy.lua | 0
Amodules/home/hyprland.nix | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/kitty/scrollback.lua -> modules/home/kitty/scrollback.lua | 0
Amodules/home/media.nix | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/mpv/shaders/CfL_Prediction.LICENSE -> modules/home/mpv/shaders/CfL_Prediction.LICENSE | 0
Rhome/mpv/shaders/CfL_Prediction.glsl -> modules/home/mpv/shaders/CfL_Prediction.glsl | 0
Amodules/home/neovim.nix | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/prompt.nix | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/rmpc/config.ron -> modules/home/rmpc/config.ron | 0
Rhome/rmpc/scripts/fetch-lyrics -> modules/home/rmpc/scripts/fetch-lyrics | 0
Rhome/rmpc/themes/rose-pine.ron -> modules/home/rmpc/themes/rose-pine.ron | 0
Amodules/home/session.nix | 39+++++++++++++++++++++++++++++++++++++++
Amodules/home/shell.nix | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/sops.nix | 48++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/ssh.nix | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/terminal.nix | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/tools.nix | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/home/yazi.nix | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhome/yazi/flavors/rose-pine.yazi/flavor.toml -> modules/home/yazi/flavors/rose-pine.yazi/flavor.toml | 0
Rhome/yazi/flavors/rose-pine.yazi/tmtheme.xml -> modules/home/yazi/flavors/rose-pine.yazi/tmtheme.xml | 0
Amodules/hosts/laptop/configuration.nix | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/default.nix | 17+++++++++++++++++
Amodules/hosts/laptop/fan-cli.nix | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/fan-extras.nix | 30++++++++++++++++++++++++++++++
Rhosts/laptop/fan-reference/99-cpu-freq-cap.conf -> modules/hosts/laptop/fan-reference/99-cpu-freq-cap.conf | 0
Rhosts/laptop/fan-reference/README.md -> modules/hosts/laptop/fan-reference/README.md | 0
Rhosts/laptop/fan-reference/fan-ctl -> modules/hosts/laptop/fan-reference/fan-ctl | 0
Rhosts/laptop/fan-reference/fan-state -> modules/hosts/laptop/fan-reference/fan-state | 0
Rhosts/laptop/fan-reference/fanfix-fan.service -> modules/hosts/laptop/fan-reference/fanfix-fan.service | 0
Rhosts/laptop/fan-reference/fanfix.conf -> modules/hosts/laptop/fan-reference/fanfix.conf | 0
Rhosts/laptop/fan-reference/modules-load.conf -> modules/hosts/laptop/fan-reference/modules-load.conf | 0
Rhosts/laptop/fan-reference/motherboard-stability.service -> modules/hosts/laptop/fan-reference/motherboard-stability.service | 0
Rhosts/laptop/fan-reference/uniwill-laptop.conf -> modules/hosts/laptop/fan-reference/uniwill-laptop.conf | 0
Amodules/hosts/laptop/fan-throttle-guard.nix | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhosts/laptop/fanfix -> modules/hosts/laptop/fanfix | 0
Amodules/hosts/laptop/hardware.nix | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/nix-settings.nix | 46++++++++++++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/nvidia.nix | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/sops.nix | 37+++++++++++++++++++++++++++++++++++++
Amodules/hosts/laptop/ssd.nix | 35+++++++++++++++++++++++++++++++++++
Rhosts/laptop/stability_guard.py -> modules/hosts/laptop/stability_guard.py | 0
Amodules/hosts/laptop/toolbox.nix | 31+++++++++++++++++++++++++++++++
Amodules/hosts/laptop/uniwill-laptop.nix | 25+++++++++++++++++++++++++
Rhosts/laptop/uniwill-laptop/Makefile -> modules/hosts/laptop/uniwill-laptop/Makefile | 0
Rhosts/laptop/uniwill-laptop/package.nix -> modules/hosts/laptop/uniwill-laptop/_package.nix | 0
Rhosts/laptop/uniwill-laptop/uniwill-acpi.c -> modules/hosts/laptop/uniwill-laptop/uniwill-acpi.c | 0
Rhosts/laptop/uniwill-laptop/uniwill-wmi.c -> modules/hosts/laptop/uniwill-laptop/uniwill-wmi.c | 0
Rhosts/laptop/uniwill-laptop/uniwill-wmi.h -> modules/hosts/laptop/uniwill-laptop/uniwill-wmi.h | 0
Amodules/parts.nix | 9+++++++++
Dmodules/workstation.nix | 43-------------------------------------------
Ascripts/wrap.sh | 24++++++++++++++++++++++++
119 files changed, 4830 insertions(+), 3472 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -4,3 +4,7 @@ result-* secrets/*.dec secrets/*.plain* keys.txt + +# scratch of the plan executor and a stray empty clone, never flake source +.superpowers/ +/NixDaemon/ diff --git a/README.md b/README.md @@ -38,69 +38,73 @@ into `$HOME`, so editing the checkout edits the live config. ```text NixDaemon/ -├── flake.nix inputs: nixpkgs (unstable), nixpkgs-stable (26.05, bootstrap only), home-manager, +├── flake.nix inputs: nixpkgs (unstable), flake-parts, import-tree, wrapper-modules, home-manager, │ hyprland, caelestia-shell, caelestia-cli, llm-agents, sops-nix, nvf -│ outputs: nixosConfigurations.nixos (the target) and .bootstrap (stage A) +│ outputs = flake-parts.lib.mkFlake { inherit inputs; } (import-tree ./modules) ├── .sops.yaml, secrets/ sops-nix: the age recipient and the encrypted secrets file -├── hosts/laptop/ the machine -│ ├── default.nix boot, users (zsh login shell), greetd/tuigreet, Hyprland (uwsm), audio, fonts, -│ │ portals, nix-ld, LocalSend port, session environment -│ ├── fan-throttle-guard.nix vault gpu-fan-fix/, imported unchanged; fanfix + stability_guard.py beside it -│ ├── fan-extras.nix the performance power profile (fanfix install did this by hand on Arch) -│ ├── uniwill-laptop.nix the `uniwill` hwmon the guard reads: kernel 6.19 driver built for this kernel -│ ├── nvidia.nix open kernel module, panel on the dGPU, colon-free DRM device names for Hyprland -│ ├── ssd.nix Samsung 980 crypttab + /mnt/ssd (key restored by hand, or via sops) -│ ├── nix-settings.nix flakes, hyprland.cachix.org, nh (Nix helper) + weekly clean, nvd, nom -│ ├── toolbox.nix envfs (foreign shebangs), ~/.local/bin first on PATH, padx udev rule -│ ├── sops.nix sops-nix for the system: secrets/secrets.yaml → /run/secrets -│ ├── fan-cli.nix fan-ec: EC fan control as a store script, passwordless sudo for wheel -│ ├── fan-reference/ the Arch-era captures (fan-ctl, fan-state, units, confs) and their README -│ └── uniwill-laptop/ the driver sources (uniwill-acpi.c, uniwill-wmi.c) and their package.nix -├── hosts/bootstrap/ stage A: the GNOME install + fan fix + toolbox prerequisites (delete after stage B) -├── home/ home-manager for daemonsec -│ ├── default.nix imports the modules below -│ ├── modules/hyprland.nix Lua config wiring, helper scripts (wallpaper-picker, keybinds-menu, …), polkit, cliphist -│ ├── modules/caelestia.nix programs.caelestia: shell.json, the CLI with both Rosé Pine schemes, Dawn as the state -│ ├── modules/terminal.nix kitty: DMMono Nerd Font, Rosé Pine dark, tmux-style ctrl+a keys, Neovim copy mode -│ ├── modules/shell.nix zsh wiring (ZDOTDIR → dotfiles, fzf bindings, completions), tmux plugins, secretspec -│ ├── modules/dotfiles.nix every dotfile from ~/git/daemon-sec-dotfiles as an out-of-store symlink -│ ├── modules/tools.nix toolbox runtime closure, python env, the general CLI tools, `ns` (package search) -│ ├── modules/cheats.nix the cheat cards: `nix-cheat` (rebuild, nh, secrets) and `gpg-cheat` (home/cheats/*.md) -│ ├── modules/sops.nix sops-nix for the user; sops, age, ssh-to-age -│ ├── modules/neovim.nix nvf: Neovim with a small Nix-built plugin set, Rosé Pine, LSPs for this machine's languages -│ ├── modules/prompt.nix starship (two-line, one Rosé Pine colour per section) and fastfetch (NixOS logo) -│ ├── modules/fan.nix `fan`: status and watch without root; max/auto with the clamp watchdog -│ ├── modules/ssh.nix the ssh key (private half from sops) and ~/.ssh/config -│ ├── modules/gpg.nix the GPG main key: public in home/gpg/, secret from sops, gpg.conf, trust -│ ├── modules/git.nix git identity, signing with the main key, aliases, delta -│ ├── modules/media.nix mpd user service, rmpc (full config, Rosé Pine, lyrics), mpv (gpu-next, uosc, thumbfast) -│ ├── modules/yazi.nix yazi: inline image/video/pdf previews, openers, Rosé Pine, git + full-border plugins -│ ├── modules/gtk.nix Yaru-purple icons, cursor, prefer-dark -│ ├── hypr/*.lua omarchy (shim) · core · looknfeel · defaults · bindings · lid · caelestia -│ ├── kitty/scrollback.lua kitty copy mode as a Neovim buffer -│ ├── caelestia/ scheme.json (dark) · scheme-dawn.json · rose-pine-{dark,dawn}.txt · shell-tokens.json -│ ├── cheats/*.md the cards: nix.md, gpg.md -│ ├── gpg/daemon-main.pub.asc the main key's public half -│ ├── rmpc/, mpv/shaders/, yazi/flavors/ the carried rmpc config and theme, the CfL shader, the Rosé Pine flavor -└── modules/workstation.nix Claude Code, Claude desktop, Obsidian, gh, glab +├── scripts/wrap.sh turns a plain module file into a flake-parts module (how the tree below was made) +└── modules/ every *.nix here is a flake-parts module; paths containing /_ are skipped + ├── parts.nix systems; the home-manager and wrapper-modules flake modules + ├── hosts/laptop/ the machine — flake.nixosModules.laptop-* and the nixosConfiguration + │ ├── default.nix flake.nixosConfigurations.nixos = nixosSystem { modules = [ self.nixosModules.laptop ]; } + │ ├── configuration.nix self.nixosModules.laptop: imports every module below by name; daemon.desktop.* switches; + │ │ boot, users (zsh login shell), greetd/tuigreet, audio, fonts, portals, nix-ld, LocalSend port + │ ├── hardware.nix laptop-hardware (nixos-generate-config output, unchanged) + │ ├── fan-throttle-guard.nix laptop-fan-throttle-guard: vault gpu-fan-fix/, unchanged; fanfix + stability_guard.py beside it + │ ├── fan-extras.nix laptop-fan-extras: the performance power profile + │ ├── uniwill-laptop.nix laptop-uniwill: the `uniwill` hwmon the guard reads (uniwill-laptop/_package.nix, sources) + │ ├── nvidia.nix laptop-nvidia: open kernel module, panel on the dGPU, colon-free DRM names for Hyprland + │ ├── ssd.nix laptop-ssd: Samsung 980 crypttab + /mnt/ssd + │ ├── nix-settings.nix laptop-nix-settings: flakes, hyprland.cachix.org, nh + weekly clean, nvd, nom + │ ├── toolbox.nix laptop-toolbox: envfs, ~/.local/bin first on PATH, padx udev rule + │ ├── sops.nix laptop-sops: secrets/secrets.yaml → /run/secrets + │ ├── fan-cli.nix laptop-fan-cli: fan-ec, passwordless sudo for wheel + │ └── fan-reference/ the Arch-era captures and their README + ├── features/ shared NixOS features, by name + │ ├── workstation.nix workstation: Claude Code, Claude desktop, Obsidian, gh, glab + │ ├── home-manager.nix home-manager: HM as a NixOS module, users.daemonsec = self.homeModules.daemonsec + │ └── desktop/ + │ ├── options.nix desktop-options: daemon.desktop.hyprland.enable / daemon.desktop.niri.enable (both default true) + │ ├── hyprland.nix desktop-hyprland: programs.hyprland (uwsm), the GTK portal — gated + │ ├── niri.nix packages.niri (wrapper-modules: config.kdl from Nix, binds, Rosé Pine) + desktop-niri — gated + │ └── noctalia.nix packages.noctalia (wrapper-modules: settings.json from Nix, Rosé Pine "Rosepine" scheme) + └── home/ flake.homeModules.* — the user's home + ├── default.nix homeModules.daemonsec: imports every module below by name + ├── hyprland.nix Lua config wiring, helper scripts, polkit, cliphist — only with daemon.desktop.hyprland + ├── caelestia.nix programs.caelestia: shell.json, the CLI with both Rosé Pine schemes — same gate + ├── terminal.nix kitty: DMMono Nerd Font, Rosé Pine dark, tmux-style ctrl+a keys, Neovim copy mode + ├── shell.nix zsh wiring (ZDOTDIR → dotfiles, fzf bindings, completions), tmux plugins, secretspec + ├── dotfiles.nix every dotfile from ~/git/daemon-sec-dotfiles as an out-of-store symlink + ├── tools.nix toolbox runtime closure, python env, the general CLI tools, `ns` (package search) + ├── cheats.nix the cheat cards: `nix-cheat` and `gpg-cheat` (cheats/*.md) + ├── sops.nix sops-nix for the user; sops, age, ssh-to-age + ├── neovim.nix nvf: Neovim with a small Nix-built plugin set, Rosé Pine + ├── prompt.nix starship and fastfetch + ├── fan.nix `fan`: status and watch without root; max/auto with the clamp watchdog + ├── ssh.nix, gpg.nix, git.nix keys from sops, ~/.ssh/config, gpg.conf, git identity and signing + ├── media.nix mpd, rmpc, mpv + ├── yazi.nix, gtk.nix yazi with previews and Rosé Pine; Yaru-purple icons, cursor, prefer-dark + ├── hypr/*.lua omarchy (shim) · core · looknfeel · defaults · bindings · lid · caelestia + ├── kitty/, caelestia/, cheats/, gpg/, rmpc/, mpv/, yazi/ the data those modules read ``` ## What runs | Layer | Choice | Where | |---|---|---| -| Boot, login | systemd-boot · greetd + tuigreet (remembers user and session) | hosts/laptop/default.nix | -| Compositor | Hyprland 0.56 under uwsm, configured in Lua; springy overshoot animations, square corners, blur | home/hypr/, home/modules/hyprland.nix | -| Shell UI | Caelestia Shell: bar, launcher, dashboard, sidebar, lock, OSD, notifications. Rosé Pine dark, framed bar, 340 px panels | home/modules/caelestia.nix, home/caelestia/ | -| Terminal | kitty, DMMono Nerd Font, Rosé Pine dark, `ctrl+a` tmux-style prefix, copy mode in Neovim | home/modules/terminal.nix | -| Shell | zsh as login shell; config = the dotfiles' `~/.dotfiles` tree (autosuggestions, syntax highlighting, fzf-tab, zoxide, atuin). Prompt and fetch are Nix-managed | home/modules/shell.nix, prompt.nix | -| Dotfiles | symlinked from `~/git/daemon-sec-dotfiles/home` | home/modules/dotfiles.nix | -| Editor | Neovim via nvf: treesitter, LSP, blink completion, telescope, oil, gitsigns, which-key, Rosé Pine | home/modules/neovim.nix | -| Tools | uv, nodejs, btop, yazi, lazygit, tmux, ripgrep/fd/fzf/bat/eza, delta, tldr, LocalSend, … | home/modules/tools.nix | -| Secrets | sops-nix (age) for the repo; secretspec (keyring) for projects | hosts/laptop/sops.nix, home/modules/sops.nix, shell.nix | -| GPU | NVIDIA open module, panel on the RTX; Aquamarine told the GPUs by colon-free udev symlinks | hosts/laptop/nvidia.nix | -| Fan | `fan-throttle-guard`: 3.2 GHz floor, EC auto fan, `uniwill` hwmon from a 6.19 driver. `fan` command: status, watch, max/auto with a clamp watchdog | hosts/laptop/fan-*.nix, uniwill-laptop/, home/modules/fan.nix | -| Nix | flakes, Hyprland cache, `nh` with weekly `clean all --keep 5 --keep-since 14d` | hosts/laptop/nix-settings.nix | +| Boot, login | systemd-boot · greetd + tuigreet (remembers user and session) | modules/hosts/laptop/configuration.nix | +| Compositor | Hyprland 0.56 under uwsm, configured in Lua; springy overshoot animations, square corners, blur | modules/home/hypr/, modules/home/hyprland.nix | +| Shell UI | Caelestia Shell: bar, launcher, dashboard, sidebar, lock, OSD, notifications. Rosé Pine dark, framed bar, 340 px panels | modules/home/caelestia.nix, modules/home/caelestia/ | +| Second desktop | Niri + Noctalia Shell (Rosé Pine "Rosepine"), both as wrapped packages: `nix run ~/NixDaemon#niri` / `#noctalia`. Pick the session in tuigreet; `daemon.desktop.{hyprland,niri}.enable` in configuration.nix drop one | modules/features/desktop/ | +| Terminal | kitty, DMMono Nerd Font, Rosé Pine dark, `ctrl+a` tmux-style prefix, copy mode in Neovim | modules/home/terminal.nix | +| Shell | zsh as login shell; config = the dotfiles' `~/.dotfiles` tree (autosuggestions, syntax highlighting, fzf-tab, zoxide, atuin). Prompt and fetch are Nix-managed | modules/home/shell.nix, prompt.nix | +| Dotfiles | symlinked from `~/git/daemon-sec-dotfiles/home` | modules/home/dotfiles.nix | +| Editor | Neovim via nvf: treesitter, LSP, blink completion, telescope, oil, gitsigns, which-key, Rosé Pine | modules/home/neovim.nix | +| Tools | uv, nodejs, btop, yazi, lazygit, tmux, ripgrep/fd/fzf/bat/eza, delta, tldr, LocalSend, … | modules/home/tools.nix | +| Secrets | sops-nix (age) for the repo; secretspec (keyring) for projects | modules/hosts/laptop/sops.nix, modules/home/sops.nix, shell.nix | +| GPU | NVIDIA open module, panel on the RTX; Aquamarine told the GPUs by colon-free udev symlinks | modules/hosts/laptop/nvidia.nix | +| Fan | `fan-throttle-guard`: 3.2 GHz floor, EC auto fan, `uniwill` hwmon from a 6.19 driver. `fan` command: status, watch, max/auto with a clamp watchdog | modules/hosts/laptop/fan-*.nix, uniwill-laptop/, modules/home/fan.nix | +| Nix | flakes, Hyprland cache, `nh` with weekly `clean all --keep 5 --keep-since 14d` | modules/hosts/laptop/nix-settings.nix | ## Setting it up @@ -133,19 +137,24 @@ nix flake show "$REPO" 1. Boot the NixOS live ISO, partition and mount at `/mnt`; generate `hardware-configuration.nix` with `nixos-generate-config --root /mnt` and - compare it with `hosts/laptop/hardware-configuration.nix` (UUIDs). -2. `git clone git@gitlab.com:DAEMON-404/NixDaemon.git && cd NixDaemon`, drop - the new hardware file into `hosts/laptop/`, `git add` it. + compare it with `modules/hosts/laptop/hardware.nix` (UUIDs). +2. `git clone git@gitlab.com:DAEMON-404/NixDaemon.git && cd NixDaemon`, paste + the generated file's body into `modules/hosts/laptop/hardware.nix` (inside + the `flake.nixosModules.laptop-hardware =` wrapper). Do not drop a raw + `hardware-configuration.nix` into `modules/`: import-tree would load it as a + flake-parts module and evaluation would fail. 3. `sudo nixos-install --flake .#nixos`, reboot, log in at tuigreet, pick **Hyprland (UWSM)** once. 4. Clone the dotfiles to `~/git/daemon-sec-dotfiles` (the links in - `home/modules/dotfiles.nix` point there) and the toolbox to `~/.local/bin`. + `modules/home/dotfiles.nix` point there) and the toolbox to `~/.local/bin`. 5. Restore the age key to `~/.config/sops/age/keys.txt` and copy it for the system (see Secrets), then the Samsung SSD key (below). -### The staged migration this repo was built for (2026-10-07) +### The staged migration this repo was built for (2026-10-07, done) -Everything below needs `sudo`, so it was left to the owner. +History, kept as a record. Stage A (`#bootstrap`, built from `nixpkgs-stable`) +no longer exists: the dendritic rewrite of 2026-10-08 removed it, so none of +the `#bootstrap` commands below work any more. **Stage A: fan fix now, on the GNOME install.** Small switch (fan module, driver, toolbox prerequisites, Hyprland cache). The new kernel modules only @@ -195,7 +204,7 @@ caelestia.lua takes over carry their Caelestia descriptions; the stock Obsidian and YouTube lines are gone because vault-open and bakx own those keys; the two webcam-overlay binds were not carried (keycodes unknown). -Afterwards delete `hosts/bootstrap/` and the `nixpkgs-stable` input. +`hosts/bootstrap/` and the `nixpkgs-stable` input were deleted on 2026-10-08. **Samsung SSD key** (vault samsung-ssd.md, section 2; needs the gpg passphrase): @@ -209,13 +218,13 @@ sudo systemctl restart systemd-cryptsetup@ssd.service mnt-ssd.mount # or just Until then the drive stays locked; both units are `nofail`, so boot is unaffected. The sops way: `sops set secrets/secrets.yaml '["ssd.key"]' "\"$(gpg -d ssd.key.gpg)\""`, -then `sops.secrets."ssd.key".path = "/etc/secrets/ssd.key";` in hosts/laptop/sops.nix. +then `sops.secrets."ssd.key".path = "/etc/secrets/ssd.key";` in modules/hosts/laptop/sops.nix. ## The shell -zsh is the login shell (hosts/laptop/default.nix) and its configuration is +zsh is the login shell (modules/hosts/laptop/configuration.nix) and its configuration is the dotfiles checkout's `home/.dotfiles` tree, reached through a Nix-managed -`~/.zshenv` that sets `ZDOTDIR=~/.dotfiles` (home/modules/shell.nix). The +`~/.zshenv` that sets `ZDOTDIR=~/.dotfiles` (modules/home/shell.nix). The plugins (autosuggestions, syntax highlighting, zsh-completions, fzf-tab, history-substring-search, you-should-use) are the copies vendored in that tree; starship, zoxide, atuin and fzf come from nixpkgs. Edit @@ -226,7 +235,7 @@ key bindings from the store, core.zsh only knows the Arch paths), the tool configs linked into `~/.config` by dotfiles.nix (bat theme, atuin, crossfetch for the splash animation, cheats, eza, btop, yazi, lazygit, carapace, and the rest of the checkout's .config; the starship prompt and the fastfetch card are -Nix-managed in home/modules/prompt.nix), `~/.tmux.conf` with +Nix-managed in modules/home/prompt.nix), `~/.tmux.conf` with its plugins from nixpkgs behind a TPM stand-in, and the binaries modern.zsh aliases (btop, dust, duf, procs, delta, tldr, hyperfine, glow, onefetch, lazygit, yazi, neovim). Not carried: `mise` (its downloaded runtimes @@ -236,7 +245,7 @@ machine; `~/.gitconfig` stays). ## The dotfiles -`home/modules/dotfiles.nix` mirrors `~/git/daemon-sec-dotfiles/home/` into +`modules/home/dotfiles.nix` mirrors `~/git/daemon-sec-dotfiles/home/` into `$HOME` with out-of-store symlinks: editing the checkout edits the live config, and a rebuild is only needed to add or remove a path in the list. The header of that file names what is deliberately not linked (hypr and @@ -260,7 +269,7 @@ and envfs resolves the `#!/bin/bash` and `#!/usr/bin/python3` shebangs. quietly); `clipboard-backup` and `omarchy-menu-tmux-keybindings` are bound but not in the carried `bin/`. - The cheat popups (`omarchy-menu-kitty`, …) pipe into `omarchy-menu-select`, - provided here as a fuzzel wrapper (home/modules/hyprland.nix). `nix-cheat` + provided here as a fuzzel wrapper (modules/home/hyprland.nix). `nix-cheat` and `gpg-cheat` are this repo's own cards, in the same style. ## Secrets @@ -268,8 +277,8 @@ and envfs resolves the `#!/bin/bash` and `#!/usr/bin/python3` shebangs. Two tools. **sops-nix** keeps secrets *in this repo*, encrypted with age (`.sops.yaml` names the recipient, `secrets/secrets.yaml` holds the values) and decrypts them at activation: `/run/secrets/NAME` for the system -(hosts/laptop/sops.nix), `~/.config/sops-nix/secrets/NAME` for the user -(home/modules/sops.nix). The age key is `~/.config/sops/age/keys.txt`, +(modules/hosts/laptop/sops.nix), `~/.config/sops-nix/secrets/NAME` for the user +(modules/home/sops.nix). The age key is `~/.config/sops/age/keys.txt`, created on 2026-10-08 and **not in the repo**; back it up (vault) and give the system its copy once: @@ -281,8 +290,8 @@ Edit with `sops secrets/secrets.yaml`, declare with `sops.secrets.NAME = { };`, rebuild. `nix-cheat secrets` has the commands. What the file holds today: `ssh_id_ed25519` (the SSH private key, used by -home/modules/ssh.nix), `gpg_main_secret` (the armored GPG secret key, still -under its own passphrase, imported by home/modules/gpg.nix on first +modules/home/ssh.nix), `gpg_main_secret` (the armored GPG secret key, still +under its own passphrase, imported by modules/home/gpg.nix on first activation), and `example`. So a fresh install needs exactly one secret restored by hand, the age key; ssh, gpg and git then come up from the flake. @@ -298,10 +307,10 @@ add NAME`, `secretspec check`, `secretspec run -- cmd`. framed bar: a 10 px border with 25 px rounded inner corners, clock and tray in pills, filled occupied workspaces, the Nix snowflake as the logo. Sidebar, utilities and notification panels are narrower than stock - (`home/caelestia/shell-tokens.json`). A Dawn mapping is registered too: + (`modules/home/caelestia/shell-tokens.json`). A Dawn mapping is registered too: `caelestia scheme set -n rose-pine -f rose-pine-dawn`. - **Bar shows the program**, not the window title: a small patch to the - shell's ActiveWindow component (`home/caelestia/active-window-program-name.patch`, + shell's ActiveWindow component (`modules/home/caelestia/active-window-program-name.patch`, applied in caelestia.nix) makes compact mode use the desktop entry's name for the window class. The shell compiles locally because of it. - **More shell**: desktop clock on the wallpaper (bottom right), audio @@ -310,17 +319,17 @@ add NAME`, `secretspec check`, `secretspec run -- cmd`. the wallpaper, a toast on track change, vim keys in the launcher, and idle: lock after 15 min, screen off after 20 (audio or an inhibitor holds both off). -- **Springy windows**: `home/hypr/looknfeel.lua` carries the dotfiles' +- **Springy windows**: `modules/home/hypr/looknfeel.lua` carries the dotfiles' animation rice (overshoot curves on move/resize/open, shadows, blur tuning, drag snapping, swallow, 3-finger workspace swipe, 4-finger-up fullscreen). Loaded after core.lua. -- **kitty, tmux-style** (`ctrl+a` prefix; table in home/modules/terminal.nix): +- **kitty, tmux-style** (`ctrl+a` prefix; table in modules/home/terminal.nix): `c` tab, `-`/`|` splits, `hjkl` panes, `z` zoom, `[` copy mode in Neovim (`v` select, `y` copy, Enter copy and leave, `q`), `]` paste, `1..9` tabs, `ctrl+a ctrl+a` sends a real ctrl+a to the shell. - `CTRL+SUPER+SPACE` opens the Caelestia launcher in its wallpaper grid (helper `wallpaper-picker`); `>wallpaper name` filters. The directory is - `paths.wallpaperDir` in home/modules/caelestia.nix. + `paths.wallpaperDir` in modules/home/caelestia.nix. - Keys to try first: SUPER+RETURN terminal, SUPER+SPACE launcher, SUPER+ESCAPE session menu, SUPER+K keybindings list, SUPER+M window mode, SUPER+` dropdown terminal, PRINT screenshot. @@ -330,7 +339,7 @@ add NAME`, `secretspec check`, `secretspec run -- cmd`. `stability_guard.py` reads the `uniwill` hwmon (board GPU temperature, main fan rpm) and falls back to a permanent 1.8 GHz ceiling without it. The driver was merged upstream in Linux 6.19; nixpkgs' 6.18 kernel predates it and the -7.2 kernel config leaves its Kconfig submenu off. `hosts/laptop/uniwill-laptop/` +7.2 kernel config leaves its Kconfig submenu off. `modules/hosts/laptop/uniwill-laptop/` holds the v6.19 sources and builds them as an out-of-tree module against whatever kernel is selected (verified on 6.18.55). Revisit when the default NixOS kernel ships it. @@ -339,7 +348,7 @@ NixOS kernel ships it. - **ANSI green**: Rosé Pine puts pine (#31748f) in the green slot; the rule says pine is never ink. kitty uses foam (#9ccfd8) for color2/color10 - meanwhile; one variable in home/modules/terminal.nix. + meanwhile; one variable in modules/home/terminal.nix. - **Display manager**: greetd + tuigreet chosen (text greeter, remembers user and session). sddm would be a one-file swap. - **GPU / MUX**: configured for what the firmware presents, the panel on the @@ -348,7 +357,7 @@ NixOS kernel ships it. - **Hostname** stays `nixos` (the installer's). The flake output is also `nixos`. - Stock Omarchy keys whose program is still not installed (spotify, 1password, signal) show a notification saying so. Add packages to - home/modules/tools.nix when wanted. + modules/home/tools.nix when wanted. --- diff --git a/docs/superpowers/plans/2026-10-08-dendritic-niri-noctalia.md b/docs/superpowers/plans/2026-10-08-dendritic-niri-noctalia.md @@ -0,0 +1,375 @@ +# Dendritic NixDaemon with Niri + Noctalia Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn `~/NixDaemon` into a flake-parts + import-tree (dendritic) flake and add a Niri + Noctalia desktop beside Hyprland + Caelestia, each switchable with one boolean. + +**Architecture:** `flake.nix` becomes `flake-parts.lib.mkFlake { inherit inputs; } (import-tree ./modules)`; every file under `modules/` is a flake-parts module that declares `flake.nixosModules.<name>`, `flake.homeModules.<name>`, `flake.nixosConfigurations.nixos` or `perSystem.packages.<name>`, and everything is wired by name through `self`. Existing NixOS/home-manager module bodies move unchanged. Niri and Noctalia are wrapper-modules packages (`perSystem.packages.niri` / `.noctalia`) consumed by a gated `programs.niri` NixOS module. + +**Tech Stack:** NixOS unstable, flake-parts, import-tree, `github:BirdeeHub/nix-wrapper-modules`, home-manager (as a NixOS module), niri 26.04, noctalia-shell 4.7.7. + +**Spec:** `docs/superpowers/specs/2026-10-08-dendritic-niri-noctalia-design.md` + +## Global Constraints + +- Module bodies are carried over unchanged; only the wrapping and relative paths change. +- Rosé Pine Main only (base `#191724`, rose `#ebbcba`, overlay `#26233a`); never Moon. +- import-tree imports every `*.nix` under `modules/`; a path containing `/_` is skipped. Non-module `.nix` files must be renamed with a leading underscore. +- New files are invisible to the flake until `git add`; every task ends with `git add -A`. No commits: the owner commits with jj. +- `bootstrap` configuration and the `nixpkgs-stable` input are deleted. +- Nothing is switched live until Task 8; `nvd diff` must show no removals before `nh os switch`. +- Working directory for every command: `~/NixDaemon`. + +## Review Focus + +1. A home module referencing a data file by the old `../x` path evaluates to a missing-path error only when that option is used; the toplevel build in Task 5 exercises all of them. Pinned by Task 5 step 3. +2. `daemon.desktop.hyprland.enable = false` must drop the Hyprland session and Caelestia from the closure without an evaluation error (the Hyprland and Caelestia HM modules from `sharedModules` still exist and must stay inert). Pinned by Task 4 step 6. +3. Both switches `false` must fail evaluation with the assertion message, not build a system with no login session. Pinned by Task 4 step 7. +4. The Niri config must pass `niri validate` (the wrapper runs it at build time); a typo in a bind name fails the build, not the login. Pinned by Task 7 step 3. +5. Noctalia must start with Rosé Pine without a writable config dir (settings come from the store). Pinned by Task 6 step 4 (nested run shows the Rosé Pine bar). + +--- + +### Task 1: Flake skeleton and inputs + +**Files:** +- Modify: `flake.nix` +- Create: `modules/parts.nix` +- Delete: `hosts/bootstrap/default.nix` + +**Interfaces:** +- Produces: inputs `flake-parts`, `import-tree`, `wrapper-modules`; flake-parts modules receive `{ self, inputs, ... }`; `perSystem` receives `{ pkgs, lib, self', ... }`; `flake.homeModules.*` exists (from home-manager's flake module); `wrappers` are reachable as `inputs.wrapper-modules.wrappers.<name>`. + +- [ ] **Step 1: Rewrite `flake.nix`** + +Keep the description and every existing input except `nixpkgs-stable`; add +```nix +flake-parts.url = "github:hercules-ci/flake-parts"; +flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; +import-tree.url = "github:vic/import-tree"; +wrapper-modules.url = "github:BirdeeHub/nix-wrapper-modules"; +``` +Replace the whole `outputs` with +```nix +outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); +``` +Carry the explanatory comments on the inputs over; drop the `bootstrap` comment block. + +- [ ] **Step 2: Create `modules/parts.nix`** + +```nix +# modules/parts.nix — flake-parts wiring shared by every module under modules/. +{ inputs, ... }: +{ + systems = [ "x86_64-linux" ]; + imports = [ + inputs.home-manager.flakeModules.home-manager # flake.homeModules / homeConfigurations + inputs.wrapper-modules.flakeModules.default # flake.wrappers, perSystem.wrappers + ]; +} +``` + +- [ ] **Step 3: Delete `hosts/bootstrap/`, move the old `modules/workstation.nix` aside** + +`git rm -r hosts/bootstrap`; `git mv modules/workstation.nix modules/_workstation.nix.old` (Task 4 turns it into `modules/features/workstation.nix`; the underscore keeps import-tree off it meanwhile). + +- [ ] **Step 4: Lock and check the empty flake** + +Run: `git add -A && nix flake lock && nix flake show 2>&1 | tail -20` +Expected: `flake.lock` gains `flake-parts`, `import-tree`, `wrapper-modules` and loses `nixpkgs-stable`; `nix flake show` prints an outputs tree with `homeModules`, `nixosModules` (empty) and `packages.x86_64-linux` (empty), no error. + +### Task 2: Host modules become `flake.nixosModules.laptop-*` + +**Files:** +- Move: `hosts/laptop/*` → `modules/hosts/laptop/*` (with `hardware-configuration.nix` → `hardware.nix`, `uniwill-laptop/package.nix` → `uniwill-laptop/_package.nix`); `hosts/laptop/default.nix` → `modules/hosts/laptop/_old-default.nix` (consumed by Task 4, then deleted) +- Create: `scripts/wrap.sh` (helper; outside `modules/`, so import-tree never sees it) + +**Interfaces:** +- Produces: `self.nixosModules.laptop-hardware`, `laptop-nvidia`, `laptop-ssd`, `laptop-nix-settings`, `laptop-sops`, `laptop-toolbox`, `laptop-fan-cli`, `laptop-fan-extras`, `laptop-fan-throttle-guard`, `laptop-uniwill`. Each is the unchanged NixOS module function `{ config, pkgs, lib, inputs, user, ... }: { … }`. + +- [ ] **Step 1: Write the wrapping helper `scripts/wrap.sh`** + +Usage: `scripts/wrap.sh FILE ATTR` rewrites FILE in place so that its leading comment block stays first, followed by +``` +{ ... }: +{ + ATTR = + <original module, every line indented by two spaces>; +} +``` +Algorithm: split at the first line that does not start with `#` (and is not blank); print the comment lines, then the header, then the rest with ` ` prefixed to non-empty lines, then `;` on its own line (indented two spaces) and `}`. Implement in bash + awk. + +- [ ] **Step 2: Move the host files** + +```bash +mkdir -p modules/hosts/laptop +git mv hosts/laptop/hardware-configuration.nix modules/hosts/laptop/hardware.nix +for f in nvidia ssd nix-settings sops toolbox fan-cli fan-extras fan-throttle-guard uniwill-laptop; do git mv hosts/laptop/$f.nix modules/hosts/laptop/$f.nix; done +git mv hosts/laptop/default.nix modules/hosts/laptop/_old-default.nix +git mv hosts/laptop/uniwill-laptop modules/hosts/laptop/uniwill-laptop +git mv modules/hosts/laptop/uniwill-laptop/package.nix modules/hosts/laptop/uniwill-laptop/_package.nix +git mv hosts/laptop/fanfix hosts/laptop/stability_guard.py modules/hosts/laptop/ +rmdir hosts/laptop hosts +``` + +- [ ] **Step 3: Wrap each file** + +`scripts/wrap.sh modules/hosts/laptop/<file>.nix flake.nixosModules.laptop-<name>` with names: `hardware`, `nvidia`, `ssd`, `nix-settings`, `sops`, `toolbox`, `fan-cli`, `fan-extras`, `fan-throttle-guard`, and `uniwill-laptop.nix` → `laptop-uniwill`. + +- [ ] **Step 4: Fix the paths that moved relative to their targets** + +- `modules/hosts/laptop/sops.nix`: `defaultSopsFile = ../../../secrets/secrets.yaml;` +- `modules/hosts/laptop/uniwill-laptop.nix`: `./uniwill-laptop/_package.nix` +- `fan-throttle-guard.nix` (`./fanfix`, `./stability_guard.py`) needs no change. + +- [ ] **Step 5: Verify the module set evaluates** + +Run: `git add -A && nix eval .#nixosModules --apply 'm: builtins.attrNames m'` +Expected: `[ "laptop-fan-cli" "laptop-fan-extras" "laptop-fan-throttle-guard" "laptop-hardware" "laptop-nix-settings" "laptop-nvidia" "laptop-sops" "laptop-ssd" "laptop-toolbox" "laptop-uniwill" ]` + +### Task 3: Home modules become `flake.homeModules.*` + +**Files:** +- Move: `home/modules/*.nix` → `modules/home/*.nix`; `home/{hypr,caelestia,kitty,cheats,gpg,rmpc,mpv,yazi}` → `modules/home/…`; `home/default.nix` → `modules/home/default.nix` (rewritten) + +**Interfaces:** +- Consumes: `osConfig` (home-manager passes the NixOS config to HM modules when run as a NixOS module). +- Produces: `self.homeModules.<name>` for `caelestia cheats dotfiles fan git gpg gtk hyprland media neovim prompt shell sops ssh terminal tools yazi`, and `self.homeModules.daemonsec` that imports all of them. + +- [ ] **Step 1: Move files** + +```bash +mkdir -p modules/home +for f in home/modules/*.nix; do git mv "$f" modules/home/; done +for d in hypr caelestia kitty cheats gpg rmpc mpv yazi; do [ -e home/$d ] && git mv home/$d modules/home/$d; done +git mv home/default.nix modules/home/default.nix +rmdir home/modules home +``` + +- [ ] **Step 2: Wrap each module** + +`scripts/wrap.sh modules/home/<name>.nix flake.homeModules.<name>` for every file except `default.nix`. + +- [ ] **Step 3: Fix relative paths** (`../x` → `./x`) + +- `hyprland.nix`: seven `../hypr/*.lua` → `./hypr/*.lua` +- `caelestia.nix`: `../caelestia/…` (five occurrences) → `./caelestia/…` +- `terminal.nix`: `../kitty/scrollback.lua` → `./kitty/scrollback.lua` +- `gpg.nix`: `../gpg/daemon-main.pub.asc` → `./gpg/…` +- `media.nix`: `../rmpc/…` (three) and `../mpv/shaders` → `./…` +- `cheats.nix`: `../cheats` → `./cheats` +- `yazi.nix`: `../yazi/flavors/rose-pine.yazi` → `./yazi/…` +- `sops.nix`: `../../secrets/secrets.yaml` → `../../secrets/secrets.yaml` is unchanged in depth (`modules/home/` is two levels below the root, as `home/modules/` was). Verify with `ls modules/home/../../secrets/secrets.yaml`. + +- [ ] **Step 4: Gate the Hyprland-only modules on the NixOS switch** + +In `modules/home/hyprland.nix` and `modules/home/caelestia.nix` the wrapped function becomes `{ config, pkgs, lib, inputs, osConfig, ... }:` and its body set is wrapped as `lib.mkIf osConfig.daemon.desktop.hyprland.enable { … }`. The `let` block above the set stays outside the `mkIf`. + +- [ ] **Step 5: Rewrite `modules/home/default.nix`** + +```nix +# modules/home/default.nix — the user's home-manager configuration: every +# home module in this directory, by name. +{ self, ... }: +{ + flake.homeModules.daemonsec = { user, ... }: { + imports = with self.homeModules; [ + hyprland caelestia terminal tools shell dotfiles cheats sops neovim prompt fan ssh gpg git media yazi gtk + ]; + home = { username = user; homeDirectory = "/home/${user}"; stateVersion = "26.05"; }; + programs.home-manager.enable = true; + xdg.enable = true; + }; +} +``` +Carry the one-line per-module comments from the old file onto the import list. + +- [ ] **Step 6: Verify** + +Run: `git add -A && nix eval .#homeModules --apply 'm: builtins.length (builtins.attrNames m)'` +Expected: `18` + +### Task 4: Features, desktop switches, host configuration + +**Files:** +- Create: `modules/features/workstation.nix` (from `modules/_workstation.nix.old`), `modules/features/home-manager.nix`, `modules/features/desktop/options.nix`, `modules/features/desktop/hyprland.nix`, `modules/hosts/laptop/configuration.nix`, `modules/hosts/laptop/default.nix` +- Delete: `modules/hosts/laptop/_old-default.nix`, `modules/_workstation.nix.old` + +**Interfaces:** +- Produces: `self.nixosModules.workstation`, `home-manager`, `desktop-options`, `desktop-hyprland`, `laptop`; `self.nixosConfigurations.nixos`; NixOS options `daemon.desktop.hyprland.enable`, `daemon.desktop.niri.enable` (bool, default true). + +- [ ] **Step 1: `modules/features/workstation.nix`** + +`git mv modules/_workstation.nix.old modules/features/workstation.nix`, then `scripts/wrap.sh modules/features/workstation.nix flake.nixosModules.workstation`. + +- [ ] **Step 2: `modules/features/desktop/options.nix`** + +Exactly the module in the spec's "The desktop switch" section, declared as `flake.nixosModules.desktop-options`. + +- [ ] **Step 3: `modules/features/desktop/hyprland.nix`** + +`flake.nixosModules.desktop-hyprland = { config, lib, pkgs, inputs, ... }: lib.mkIf config.daemon.desktop.hyprland.enable { imports … }` cannot carry `imports` inside `mkIf`, so structure it as +```nix +{ imports = [ inputs.hyprland.nixosModules.default ]; + config = lib.mkIf config.daemon.desktop.hyprland.enable { + programs.hyprland = { enable = true; withUWSM = true; xwayland.enable = true; }; + xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; + }; } +``` +These two blocks are cut from `_old-default.nix` (the `##### Desktop` section) with their comments. + +- [ ] **Step 4: `modules/features/home-manager.nix`** + +`flake.nixosModules.home-manager = { inputs, user, ... }: { imports = [ inputs.home-manager.nixosModules.home-manager ]; home-manager = { useGlobalPkgs = true; useUserPackages = true; backupFileExtension = "hm-bak"; extraSpecialArgs = { inherit inputs user; }; sharedModules = [ inputs.hyprland.homeManagerModules.default inputs.caelestia-shell.homeManagerModules.default ]; users.${user} = self.homeModules.daemonsec; }; }` — the block from the old `flake.nix`, with the comment about Caelestia/Hyprland pins. + +- [ ] **Step 5: `modules/hosts/laptop/configuration.nix` and `default.nix`** + +`configuration.nix`: `flake.nixosModules.laptop = { config, pkgs, lib, user, ... }: { imports = with self.nixosModules; [ laptop-hardware laptop-fan-throttle-guard laptop-fan-extras laptop-uniwill laptop-nvidia laptop-ssd laptop-nix-settings laptop-toolbox laptop-sops laptop-fan-cli workstation home-manager desktop-options desktop-hyprland ]; daemon.desktop = { hyprland.enable = true; niri.enable = true; }; … }` where `…` is the body of `_old-default.nix` minus its `imports` and minus the two blocks moved in Step 3. Keep the file header comment and the per-import comments. (`desktop-niri` is added to this list in Task 7.) + +`default.nix`: +```nix +{ self, inputs, ... }: +{ + flake.nixosConfigurations.nixos = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs; user = "daemonsec"; }; + modules = [ self.nixosModules.laptop ]; + }; +} +``` +Then `git rm modules/hosts/laptop/_old-default.nix`. + +- [ ] **Step 6: Verify the system evaluates, and that the Hyprland switch is inert when off** + +Run: `git add -A && nix eval .#nixosConfigurations.nixos.config.system.build.toplevel.drvPath` +Expected: a `/nix/store/…-nixos-system-nixos-….drv` path. + +Run: `nix eval --impure --expr '(builtins.getFlake (toString ./.)).nixosConfigurations.nixos.extendModules { modules = [ ({ lib, ... }: { daemon.desktop.hyprland.enable = lib.mkForce false; }) ]; }' --apply 'c: [ c.config.programs.hyprland.enable c.config.home-manager.users.daemonsec.wayland.windowManager.hyprland.enable c.config.home-manager.users.daemonsec.programs.caelestia.enable ]'` +Expected: `[ false false false ]` (mkForce: the host file sets the switch explicitly; the lambda is parenthesised because it sits in a list) + +- [ ] **Step 7: Verify the assertion** + +Run: `nix eval --impure --expr '((builtins.getFlake (toString ./.)).nixosConfigurations.nixos.extendModules { modules = [ ({ lib, ... }: { daemon.desktop.hyprland.enable = lib.mkForce false; daemon.desktop.niri.enable = lib.mkForce false; }) ]; }).config.system.build.toplevel.drvPath' 2>&1 | grep -c 'enable at least one desktop'` +Expected: `1` + +(`daemon.desktop.niri.enable` exists from Step 2 even before Task 7 wires it.) + +### Task 5: Build parity with today's system + +**Files:** none + +- [ ] **Step 1: Flake check** + +Run: `nix flake check` +Expected: exits 0. + +- [ ] **Step 2: Build and diff against the running system** + +Run: `nh os build && nvd diff /run/current-system result` +Expected: the only lines are version-neutral (`No version or selection state changes.`) or additions; the `Removed packages` section is absent. If anything is removed, the port lost a module: find which `self.*` name is missing from an import list and fix before continuing. + +- [ ] **Step 3: Home activation builds** + +Run: `nix build .#nixosConfigurations.nixos.config.home-manager.users.daemonsec.home.activationPackage --no-link --print-out-paths` +Expected: a store path (this forces every `./hypr`, `./caelestia`, `./kitty`, `./cheats`, `./gpg`, `./rmpc`, `./mpv`, `./yazi` path to resolve). + +### Task 6: Noctalia package + +**Files:** +- Create: `modules/features/desktop/noctalia.nix` + +**Interfaces:** +- Produces: `self'.packages.noctalia` / `self.packages.x86_64-linux.noctalia` (wrapped `noctalia-shell`; `lib.getExe` gives the `noctalia-shell` binary; `bin/dump-noctalia-shell` also present). + +- [ ] **Step 1: Write the module** + +```nix +{ inputs, ... }: +{ + perSystem = { pkgs, ... }: { + packages.noctalia = inputs.wrapper-modules.wrappers.noctalia-shell.wrap { + inherit pkgs; + settings = { … }; + }; + }; +} +``` +`settings` is the attribute set listed in the spec's "Noctalia" section (`colorSchemes`, `bar`, `general`, `ui`, `wallpaper` with `directory = "/home/daemonsec/git/daemon-sec-dotfiles/home/.config/omarchy/backgrounds/rose-pine-dark"`, `location`, `appLauncher`, `idle`). Add a header comment: how to export changes made in the GUI (`dump-noctalia-shell`). + +- [ ] **Step 2: Build** + +Run: `git add -A && nix build .#noctalia --print-out-paths --no-link` +Expected: a store path; `ls $(…)/bin` lists `noctalia-shell` and `dump-noctalia-shell`. + +- [ ] **Step 3: The generated settings select Rosé Pine** + +Run: `nix eval --raw .#noctalia.generatedConfig | xargs -I{} jq -c .colorSchemes {}/settings.json` +Expected: `{"darkMode":true,"predefinedScheme":"Rosepine","useWallpaperColors":false}` + +- [ ] **Step 4: Nested smoke test (manual, in the running Hyprland session)** + +Run: `nix run .#noctalia` for ten seconds; a Rosé Pine bar appears at the top; Ctrl+C stops it. (Noctalia runs under Hyprland too, so this proves the package before Niri exists.) + +### Task 7: Niri package and NixOS module + +**Files:** +- Create: `modules/features/desktop/niri.nix` +- Modify: `modules/hosts/laptop/configuration.nix` (add `desktop-niri` to imports) + +**Interfaces:** +- Consumes: `self'.packages.noctalia` (Task 6). +- Produces: `self'.packages.niri`; `self.nixosModules.desktop-niri`. + +- [ ] **Step 1: Write the module** + +```nix +{ self, inputs, ... }: +{ + perSystem = { pkgs, lib, self', ... }: { + packages.niri = inputs.wrapper-modules.wrappers.niri.wrap { + inherit pkgs; + settings = { … }; + }; + }; + flake.nixosModules.desktop-niri = { config, lib, pkgs, ... }: { + config = lib.mkIf config.daemon.desktop.niri.enable { + programs.niri = { enable = true; package = self.packages.${pkgs.stdenv.hostPlatform.system}.niri; }; + }; + }; +} +``` +`settings` implements the spec's "Niri" section: `input`, `layout`, `prefer-no-csd`, `hotkey-overlay`, `xwayland-satellite.path`, `spawn-at-startup = [ (lib.getExe self'.packages.noctalia) ]`, `environment`, and `binds` exactly as the spec's table, with every program as `lib.getExe pkgs.<x>` (kitty, firefox, nautilus, obsidian, grim, slurp, wl-clipboard's `wl-copy`, brightnessctl, playerctl; `wpctl` from `pkgs.wireplumber`). Bind syntax follows the wrapper: `"Mod+Return".spawn = [ (lib.getExe pkgs.kitty) ]; "Mod+Q".close-window = null; "Mod+1".focus-workspace = 1; "Mod+Space".spawn-sh = "${noctalia} ipc call launcher toggle";`. The Noctalia binary is `let noctalia = lib.getExe self'.packages.noctalia; in`. + +- [ ] **Step 2: Add `desktop-niri` to the laptop imports** in `modules/hosts/laptop/configuration.nix`. + +- [ ] **Step 3: Build (this runs `niri validate` on the generated config)** + +Run: `git add -A && nix build .#niri --print-out-paths --no-link` +Expected: a store path. A validation failure names the bad KDL line; fix the bind and rebuild. + +- [ ] **Step 4: The system now carries the Niri session** + +Run: `nix eval .#nixosConfigurations.nixos.config.services.displayManager.sessionPackages --apply 'l: map (p: p.name) l'` +Expected: a list naming both the Hyprland (uwsm) session package and the wrapped niri package. + +- [ ] **Step 5: Nested smoke test (manual)** + +Run: `nix run .#niri` inside Hyprland. A Niri window opens with the Noctalia bar; Alt is the modifier when nested: Alt+Return opens kitty, Alt+Space opens the launcher, Alt+Shift+E quits. + +### Task 8: Documentation, final build, hand-over + +**Files:** +- Modify: `modules/home/cheats/nix.md` (the `layout` and `add` sections), `README.md` (its layout section) + +- [ ] **Step 1: Update the cheat card and README** + +In `nix.md` `## layout`, replace the path table with the new tree (flake.nix, modules/parts.nix, modules/hosts/laptop/*, modules/features/*, modules/home/*), and in `## add` point packages at `modules/home/tools.nix`, dotfiles at `modules/home/dotfiles.nix`, a new module at "a new `modules/home/<name>.nix` declaring `flake.homeModules.<name>` + one name in `modules/home/default.nix`", plus two lines: `daemon.desktop.niri.enable` / `hyprland.enable` in `modules/hosts/laptop/configuration.nix`, and `nix run ~/NixDaemon#niri` / `#noctalia`. Do the same edit in the README's layout section. + +- [ ] **Step 2: Final parity build** + +Run: `git add -A && nix flake check && nh os build && nvd diff /run/current-system result` +Expected: additions only (`niri`, `noctalia-shell`, `xwayland-satellite`, `xdg-desktop-portal-gnome`, `quickshell`, their closure); no `Removed packages` section. + +- [ ] **Step 3: Hand over** + +Stop here. Report the diff summary and the two manual steps left to the owner: `nh os switch`, then log out, pick "niri" in tuigreet, and `nh os rollback` if anything is wrong. The owner commits with jj. diff --git a/docs/superpowers/specs/2026-10-08-dendritic-niri-noctalia-design.md b/docs/superpowers/specs/2026-10-08-dendritic-niri-noctalia-design.md @@ -0,0 +1,253 @@ +# NixDaemon: dendritic flake, Niri + Noctalia beside Hyprland + Caelestia + +Date: 2026-10-08. Status: approved in conversation, awaiting written review. + +## Goal + +Rewrite `~/NixDaemon` in the dendritic pattern (flake-parts + import-tree, every +file a flake-parts module, outputs referenced by name through `self`), and add a +second desktop, Niri with Noctalia Shell, built the way vimjoyer's video 79 does +it (wrapper-modules, so the compositor and the shell are portable packages). +Both desktops are installed and chosen at login; either can be switched off +with one boolean. Theme for the new desktop: Rosé Pine Main (dark; never Moon). + +Success: `nh os switch` builds from the new tree with no change to what the +Hyprland session does today; tuigreet lists both "Hyprland" and "niri"; +`nix run ~/NixDaemon#niri` and `#noctalia` work anywhere the flake is fetched. + +## Constraints and facts the design rests on + +- home-manager runs as a NixOS module here (`useGlobalPkgs = true`). There is + no standalone home-manager profile and none is added. +- nixpkgs unstable already ships `niri` (26.04, with the `programs.niri` NixOS + module), `noctalia-shell` (4.7.7) and `xwayland-satellite`. No new package + inputs; three new flake inputs: `flake-parts`, `import-tree`, + `wrapper-modules` (`github:BirdeeHub/nix-wrapper-modules`). +- wrapper-modules facts (read from its source): + `inputs.wrapper-modules.wrappers.niri.wrap { inherit pkgs; settings = …; }` + (settings is a freeform set translated to KDL; `binds`, `layout`, + `spawn-at-startup`, `window-rules`, `outputs`, `extraConfig` are typed; the + wrapper runs `niri validate` at build time; the package passes through + `providedSessions`). `wrappers.noctalia-shell.wrap { inherit pkgs; settings; + colors; … }`: with only `settings` set it exports `NOCTALIA_SETTINGS_FILE` + pointing into the store and ships `bin/dump-noctalia-shell`, which prints the + live settings as Nix. +- Noctalia ships a predefined "Rosepine" scheme whose dark half is Rosé Pine + Main (`mSurface #191724`, `mPrimary #ebbcba`, …). Selecting it: + `colorSchemes = { predefinedScheme = "Rosepine"; darkMode = true; + useWallpaperColors = false; }`. +- import-tree imports every `*.nix` under `modules/` recursively and ignores any + path containing `/_`. Non-Nix files are never touched. +- Caelestia is started from the Hyprland Lua config, Noctalia from Niri's + `spawn-at-startup`; the shared user services (hyprpolkitagent, cliphist, + udiskie) are bound to `graphical-session.target`, which both uwsm/Hyprland and + `niri-session` manage. Portals are configured per desktop by NixOS. This is + why running both desktops side by side is safe. + +## Decisions + +1. **Full dendritic rewrite.** Every Nix file becomes a flake-parts module. + Module *bodies* (the NixOS and home-manager settings) are carried over + unchanged except for relative paths that move with them. +2. **Both desktops installed, picked in tuigreet.** Two NixOS options, + `daemon.desktop.hyprland.enable` and `daemon.desktop.niri.enable`, both + default `true`, set in the host's `configuration.nix`. An assertion + requires at least one. Home-manager modules read them through `osConfig` + so the NixOS option is the single source of truth. +3. **`bootstrap` and `nixpkgs-stable` are removed.** Their own comment says to + delete them once `nixos` is in use. +4. **Niri and Noctalia settings live in Nix** (vimjoyer's way), not in an + out-of-store config dir. The GUI remains usable for trying settings in a + session; `dump-noctalia-shell` turns the live state into Nix to paste back. + +## Layout + +``` +flake.nix inputs; outputs = flake-parts.lib.mkFlake { inherit inputs; } (import-tree ./modules) +.sops.yaml, secrets/ unchanged, repo root +modules/ + parts.nix systems = [ "x86_64-linux" ]; imports home-manager.flakeModules.home-manager + and wrapper-modules.flakeModules.default + hosts/laptop/ + default.nix flake.nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { specialArgs = { inherit inputs; user; }; + modules = [ self.nixosModules.laptop ]; } + configuration.nix flake.nixosModules.laptop: imports every self.nixosModules.laptop-* and the features + (workstation, home-manager, desktop-hyprland, desktop-niri); sets daemon.desktop.*; + body = today's hosts/laptop/default.nix minus its imports list + hardware.nix flake.nixosModules.laptop-hardware (hardware-configuration.nix, unchanged) + nvidia.nix flake.nixosModules.laptop-nvidia + ssd.nix flake.nixosModules.laptop-ssd + nix-settings.nix flake.nixosModules.laptop-nix-settings (nh, caches) + sops.nix flake.nixosModules.laptop-sops (path to ../../../secrets/secrets.yaml) + toolbox.nix flake.nixosModules.laptop-toolbox + fan-cli.nix flake.nixosModules.laptop-fan-cli + fan-extras.nix flake.nixosModules.laptop-fan-extras + fan-throttle-guard.nix flake.nixosModules.laptop-fan-throttle-guard + uniwill-laptop.nix flake.nixosModules.laptop-uniwill + uniwill-laptop/_package.nix the kernel-module derivation; underscore so import-tree skips it + fanfix, stability_guard.py data, untouched + features/ + workstation.nix flake.nixosModules.workstation (today's modules/workstation.nix) + home-manager.nix flake.nixosModules.home-manager: imports home-manager.nixosModules.home-manager; + useGlobalPkgs, useUserPackages, backupFileExtension = "hm-bak", + extraSpecialArgs = { inherit inputs; user; }, sharedModules (hyprland + caelestia + HM modules), users.${user} = self.homeModules.daemonsec + desktop/ + options.nix flake.nixosModules.desktop-options: the two options + assertion + hyprland.nix flake.nixosModules.desktop-hyprland: today's Hyprland/greetd/uwsm/portal block + from hosts/laptop/default.nix, wrapped in mkIf daemon.desktop.hyprland.enable + niri.nix perSystem.packages.niri (wrapped) and flake.nixosModules.desktop-niri: + programs.niri = { enable; package = self'.packages.niri }, mkIf daemon.desktop.niri.enable + noctalia.nix perSystem.packages.noctalia (wrapped, Rosé Pine) + home/ + default.nix flake.homeModules.daemonsec: imports every self.homeModules.* below; + home.username/homeDirectory/stateVersion, programs.home-manager, xdg + tools.nix … yazi.nix one flake.homeModules.<name> per today's home/modules/<name>.nix, bodies unchanged + hyprland.nix flake.homeModules.hyprland, body wrapped in mkIf osConfig.daemon.desktop.hyprland.enable + caelestia.nix flake.homeModules.caelestia, same gate + hypr/, caelestia/, kitty/, cheats/, gpg/, rmpc/ data directories, moved beside their modules +docs/superpowers/specs/ this file +``` + +Naming: NixOS modules for this host are `laptop-<topic>`; shared features are +bare (`workstation`, `desktop-niri`); home modules keep today's file names. + +greetd/tuigreet is the login for both desktops, so it stays in +`configuration.nix` (host level), ungated. Only the Hyprland-specific lines +(`programs.hyprland` with uwsm, the GTK portal line that exists for Hyprland) +move to `desktop-hyprland.nix` and are gated. + +## The desktop switch + +```nix +# modules/features/desktop/options.nix +flake.nixosModules.desktop-options = { lib, config, ... }: { + options.daemon.desktop = { + hyprland.enable = lib.mkEnableOption "Hyprland with Caelestia Shell" // { default = true; }; + niri.enable = lib.mkEnableOption "Niri with Noctalia Shell" // { default = true; }; + }; + config.assertions = [{ + assertion = config.daemon.desktop.hyprland.enable || config.daemon.desktop.niri.enable; + message = "daemon.desktop: enable at least one desktop"; + }]; +}; +``` + +`configuration.nix` sets both explicitly so the choice is visible in the host +file. Switching desktops day to day is: log out, pick the other session in +tuigreet (it remembers the last one). Dropping one: set it to `false`, +`nh os switch`. + +## Niri (`modules/features/desktop/niri.nix`) + +`perSystem = { pkgs, lib, self', ... }: { packages.niri = inputs.wrapper-modules.wrappers.niri.wrap { inherit pkgs; settings = { … }; }; }` + +Settings (KDL generated by the wrapper): + +- `input`: keyboard layout `us`, options `compose:caps,shift:both_capslock_cancel` + (same as the NixOS xkb settings); touchpad `tap`, `natural-scroll`; + `focus-follows-mouse`. +- `layout`: `gaps 8`; `focus-ring { width 2; active-color "#ebbcba"; inactive-color "#26233a"; }` + (rose on overlay); `border.off`; `preset-column-widths` 1/3, 1/2, 2/3. +- `prefer-no-csd`; `hotkey-overlay.skip-at-startup`. +- `xwayland-satellite.path = lib.getExe pkgs.xwayland-satellite`. +- `spawn-at-startup = [ (lib.getExe self'.packages.noctalia) ]`. +- `environment`: `ELECTRON_OZONE_PLATFORM_HINT=auto`, `QT_QPA_PLATFORM=wayland;xcb` + (mirrors the session variables set for Hyprland). +- `binds`, mirroring the Hyprland keys that have a Niri or Noctalia counterpart: + + | Key | Action | + |---|---| + | Mod+Return | spawn kitty | + | Mod+Shift+Return, Mod+Shift+B | firefox | + | Mod+Shift+Alt+B | firefox --private-window | + | Mod+Shift+F | nautilus --new-window | + | Mod+Shift+O | obsidian | + | Mod+Shift+N | kitty -e $EDITOR | + | Mod+Space, Alt+Mod+Space | noctalia ipc call launcher toggle | + | Mod+Escape, Ctrl+Mod+P | noctalia ipc call sessionMenu toggle | + | Mod+A | noctalia ipc call controlCenter toggle | + | Mod+Comma | noctalia ipc call notifications clear | + | Ctrl+Mod+V | noctalia ipc call launcher clipboard | + | Ctrl+Mod+Space | noctalia ipc call wallpaper toggle | + | Mod+Q | close-window | + | Mod+F | maximize-column; Mod+G fullscreen-window; Mod+Shift+V toggle-window-floating | + | Mod+H / Mod+J / Mod+K / Mod+L (and arrows) | focus column left / window down / window up / column right | + | Mod+Shift+Escape | noctalia ipc call lockScreen lock | + | Mod+Shift+H/J/K/L | move column / window | + | Mod+1..9, Mod+Shift+1..9 | focus / move to workspace | + | Mod+Ctrl+H/L | set-column-width ∓5%; Mod+Ctrl+J/K set-window-height | + | Mod+WheelScrollUp/Down | focus workspace up/down | + | Print | grim -g "$(slurp)" to clipboard; Shift+Print full screen | + | XF86Audio{Raise,Lower}Volume, Mute, MicMute | wpctl | + | XF86MonBrightness{Up,Down} | brightnessctl | + | XF86Audio{Play,Pause,Next,Prev} | playerctl | + | Mod+Shift+E | quit (with confirmation) | + + Programs are referenced with `lib.getExe pkgs.<x>` so the wrapped package + carries its own dependencies, exactly as in the video. + +Then `flake.nixosModules.desktop-niri = { config, lib, pkgs, ... }: lib.mkIf config.daemon.desktop.niri.enable { programs.niri = { enable = true; package = self.packages.${pkgs.stdenv.hostPlatform.system}.niri; }; }`. +nixpkgs' `programs.niri` registers the session for tuigreet and adds the +GNOME portal, which is what Niri documents. + +## Noctalia (`modules/features/desktop/noctalia.nix`) + +`perSystem = { pkgs, ... }: { packages.noctalia = inputs.wrapper-modules.wrappers.noctalia-shell.wrap { inherit pkgs; settings = { … }; }; }` + +Settings (only the keys that differ from Noctalia's defaults): + +- `colorSchemes = { predefinedScheme = "Rosepine"; darkMode = true; useWallpaperColors = false; }` +- `bar = { position = "top"; density = "compact"; }` +- `general = { lockOnSuspend = true; }` +- `ui = { fontDefault = "Noto Sans"; fontFixed = "DMMono Nerd Font"; }` +- `wallpaper = { enabled = true; directory = "<the rose-pine-dark wallpaper dir already used by Caelestia>"; fillMode = "crop"; }` +- `location = { name = "Douglas, Isle of Man"; useFahrenheit = false; use12hourFormat = false; }` (same weather spot as Caelestia) +- `appLauncher = { terminalCommand = "kitty -e"; }` +- `idle = { enabled = true; lockTimeout = 600; }`: Noctalia's own lock screen after ten idle minutes. + +Bar widget layout stays Noctalia's default for the first build; the owner +tunes it in the GUI and pastes `dump-noctalia-shell` output back into this +file. Anything the dump adds that equals a default is left out. + +## Home-manager glue (`modules/features/home-manager.nix`) + +The block that lives in `flake.nix` today moves here unchanged, except that the +user module is `self.homeModules.daemonsec`. `extraSpecialArgs` still passes +`inputs` and `user`; `sharedModules` keeps the Hyprland and Caelestia HM +modules (they only define options; the gated home modules decide whether they +do anything). + +## Path changes that come with the move + +- `modules/hosts/laptop/sops.nix`: `defaultSopsFile = ../../../secrets/secrets.yaml`. +- `modules/home/sops.nix`: same depth change. +- `modules/home/cheats.nix`: `../cheats` becomes `./cheats`; same for + `caelestia.nix` (`./caelestia/...`), `hyprland.nix` (`./hypr/...`), + `terminal.nix` (`./kitty/...`), `gpg.nix`, `media.nix` (rmpc). +- `uniwill-laptop.nix`: `./uniwill-laptop/_package.nix`. +- `fan-throttle-guard.nix` and friends: their script paths (`./fanfix`, + `./stability_guard.py`) are unchanged because the files move with them. + +## Verification (before the live switch) + +1. `nix flake check ~/NixDaemon` evaluates every output. +2. `nh os build && nvd diff /run/current-system result`. Expected: `niri`, + `noctalia-shell`, `xwayland-satellite`, `xdg-desktop-portal-gnome` and + their closure added; the removal of nothing that exists today. Any removal + is a bug in the port, fixed before switching. +3. `nix run ~/NixDaemon#niri` inside the running Hyprland session opens Niri + nested in a window with Noctalia in it (Alt is the modifier when nested); + `nix run ~/NixDaemon#noctalia` alone works too. +4. `nh os switch`; the Hyprland session keeps working; log out; tuigreet shows + "niri"; log in; Noctalia bar appears in Rosé Pine. +5. Rollback path if anything is wrong: `nh os rollback` (or the boot menu). + +## Out of scope + +- Converting the Hyprland config itself to a wrapped package (it stays a + home-manager module; it works and the video does not cover it). +- Per-project tooling, dotfiles, secrets: untouched. +- A Rosé Pine Dawn variant for Niri/Noctalia. +- Committing: the owner commits (jj). The rewrite is left as working-tree + changes plus `git add` of the new files so the flake can see them. diff --git a/flake.lock b/flake.lock @@ -150,6 +150,26 @@ "flake-parts": { "inputs": { "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790898470, + "narHash": "sha256-zTwuwezD0w3hpga6a6P8emidzAZFyWqlNeRBlpWOOl8=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "024633cd702b10285db5cb19b40ad48d2399ba60", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ "llm-agents", "nixpkgs" ] @@ -536,10 +556,25 @@ "type": "github" } }, + "import-tree": { + "locked": { + "lastModified": 1788467110, + "narHash": "sha256-ljEMTXP/rH0tOvDzc9gzwww6KcHRPRnEHzd9lK48V7s=", + "owner": "vic", + "repo": "import-tree", + "rev": "eb1b52eaecc57f7c136d07ae8a93e724dfecac46", + "type": "github" + }, + "original": { + "owner": "vic", + "repo": "import-tree", + "type": "github" + } + }, "llm-agents": { "inputs": { "bun2nix": "bun2nix", - "flake-parts": "flake-parts", + "flake-parts": "flake-parts_2", "nixpkgs": "nixpkgs_2", "systems": "systems_2", "treefmt-nix": "treefmt-nix" @@ -611,22 +646,6 @@ "type": "github" } }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1791353474, - "narHash": "sha256-v72qr4LsSz61tQki/41nHKZKPY6NSeZavtGlAInOCng=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "2efa67fd26b6df417c33e4603185c701f260dd83", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, "nixpkgs_2": { "locked": { "lastModified": 1790600678, @@ -728,13 +747,15 @@ "inputs": { "caelestia-cli": "caelestia-cli", "caelestia-shell": "caelestia-shell", + "flake-parts": "flake-parts", "home-manager": "home-manager", "hyprland": "hyprland", + "import-tree": "import-tree", "llm-agents": "llm-agents", "nixpkgs": "nixpkgs_3", - "nixpkgs-stable": "nixpkgs-stable", "nvf": "nvf", - "sops-nix": "sops-nix" + "sops-nix": "sops-nix", + "wrapper-modules": "wrapper-modules" } }, "sops-nix": { @@ -808,6 +829,26 @@ "type": "github" } }, + "wrapper-modules": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1788981141, + "narHash": "sha256-7xWS16u13YGlRNKWaAPzRbEyh4OLOXJe31k7h4AnXNU=", + "owner": "BirdeeHub", + "repo": "nix-wrapper-modules", + "rev": "1db3c116a6aa61823f8d8f3c47c306846428fc54", + "type": "github" + }, + "original": { + "owner": "BirdeeHub", + "repo": "nix-wrapper-modules", + "type": "github" + } + }, "xdph": { "inputs": { "aquamarine": [ diff --git a/flake.nix b/flake.nix @@ -1,13 +1,27 @@ { - description = "NixDaemon: NixOS + home-manager for the PCSpecialist Valeon II 17 (Hyprland, Caelestia Shell, dead-GPU-fan workaround)"; + description = "NixDaemon: NixOS + home-manager for the PCSpecialist Valeon II 17 (Hyprland + Caelestia, Niri + Noctalia, dead-GPU-fan workaround)"; + # The flake is dendritic: flake-parts evaluates every *.nix under ./modules + # (import-tree) as a flake-parts module, and each file declares the outputs + # it owns (flake.nixosModules.<name>, flake.homeModules.<name>, + # flake.nixosConfigurations.<host>, perSystem.packages.<name>). Files and + # directories whose path contains `/_` are skipped. Modules refer to each + # other by name through `self`, never by path. inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - # The release branch this machine was installed from. Only the `bootstrap` - # configuration uses it (the fan fix on today's GNOME install, see README.md). - # Delete this input together with hosts/bootstrap/ once `nixos` is in use. - nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + import-tree.url = "github:vic/import-tree"; + + # Wrapped, portable programs (modules/features/desktop/{niri,noctalia}.nix): + # `nix run ~/NixDaemon#niri` works anywhere the flake can be fetched. + wrapper-modules = { + url = "github:BirdeeHub/nix-wrapper-modules"; + inputs.nixpkgs.follows = "nixpkgs"; + }; home-manager = { url = "github:nix-community/home-manager"; @@ -15,8 +29,8 @@ }; # Deliberately not following nixpkgs: Hyprland pins its own, and the - # hyprland.cachix.org cache (hosts/laptop/nix-settings.nix) only serves - # builds made against those pins. + # hyprland.cachix.org cache (modules/hosts/laptop/nix-settings.nix) only + # serves builds made against those pins. hyprland.url = "github:hyprwm/Hyprland"; # Shell and CLI pin each other, so one revision of each is used everywhere. @@ -31,68 +45,22 @@ inputs.caelestia-shell.follows = "caelestia-shell"; }; - # Claude Code and the Claude desktop app (modules/workstation.nix). + # Claude Code and the Claude desktop app (modules/features/workstation.nix). llm-agents.url = "github:numtide/llm-agents.nix"; # Secrets: encrypted in secrets/ with age, decrypted at activation - # (hosts/laptop/sops.nix for the system, home/modules/sops.nix for the user). + # (modules/hosts/laptop/sops.nix for the system, modules/home/sops.nix for the user). sops-nix = { url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; - # Neovim, configured in Nix (home/modules/neovim.nix). + # Neovim, configured in Nix (modules/home/neovim.nix). nvf = { url = "github:notashelf/nvf"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = - inputs@{ self, nixpkgs, nixpkgs-stable, home-manager, ... }: - let - system = "x86_64-linux"; - user = "daemonsec"; - in - { - nixosConfigurations = { - # The target: Hyprland + Caelestia Shell. - # sudo nixos-rebuild boot --flake ~/NixDaemon#nixos && reboot - nixos = nixpkgs.lib.nixosSystem { - inherit system; - specialArgs = { inherit inputs user; }; - modules = [ - ./hosts/laptop - ./modules/workstation.nix - inputs.hyprland.nixosModules.default - home-manager.nixosModules.home-manager - { - home-manager = { - useGlobalPkgs = true; - useUserPackages = true; - backupFileExtension = "hm-bak"; - extraSpecialArgs = { inherit inputs user; }; - sharedModules = [ - inputs.hyprland.homeManagerModules.default - inputs.caelestia-shell.homeManagerModules.default - ]; - users.${user} = import ./home; - }; - } - ]; - }; - - # Stage A: today's GNOME install plus the fan fix and the toolbox - # prerequisites, built from the same nixpkgs the machine runs now. - # sudo nixos-rebuild switch --flake ~/NixDaemon#bootstrap && reboot - bootstrap = nixpkgs-stable.lib.nixosSystem { - inherit system; - specialArgs = { inherit inputs user; }; - modules = [ - ./hosts/bootstrap - ./modules/workstation.nix - ]; - }; - }; - }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); } diff --git a/home/cheats/gpg.md b/home/cheats/gpg.md @@ -1,321 +0,0 @@ -# gpg — the key hierarchy, and every verb - -GnuPG **2.4.9** here. Pinentry is the GNOME dialog (NixOS `programs.gnupg.agent`), `GPG_TTY` is exported by core.zsh, -and **`gpgx`** (`gpgx-cheat`) is the no-flags wrapper for the daily lock/unlock/sign/verify. This card is the real thing underneath. -Terminal card: **`gpg-cheat`** — sections `model setup keygen subkeys backup import sign verify encrypt decrypt edit revoke ssh git keyservers trust inspect offline agent fix mine`. -`FPR` below = a full 40-hex fingerprint. Yours (the main key): `3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4`. - -## model — one primary key, several subkeys - -```text -primary key [C] certify = the identity. Signs your own subkeys and user IDs, and other people's keys. - Keep it OFFLINE once the subkeys exist; it only comes out to add/revoke subkeys or UIDs. -subkey [S] sign = signs files, commits, mail. Rotate yearly without touching the identity. -subkey [E] encrypt = what others encrypt TO. Rotating it does not re-encrypt old mail; keep old ones to read it. -subkey [A] authenticate = SSH login (gpg-agent as the ssh agent). -user ID "Name <mail>" = one per address; the primary one is what people see first. -``` - -- Capabilities show in `gpg -K` as `[SC]`, `[E]`, `[A]`. A primary with `[SC]` signs with the identity key itself, which is fine but not rotatable. -- The **fingerprint** (40 hex) is the key; the "key ID" is its last 16 (long) or 8 (short, collidable — never use). -- `sec` = secret primary present. `sec#` = primary absent (offline), only subkeys here. `ssb` = secret subkey. -- Ownertrust (how much you trust a key to vouch for others) is separate from validity (is this key really theirs). `[ultimate]` is your own. - -## setup — ~/.gnupg and the agent - -```sh -# ~/.gnupg/gpg.conf (create it; sane modern defaults) -keyid-format 0xlong -with-fingerprint -with-subkey-fingerprints -default-key 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 -default-recipient-self # `gpg -e file` encrypts to you when no -r given -personal-cipher-preferences AES256 AES192 AES -personal-digest-preferences SHA512 SHA384 SHA256 -cert-digest-algo SHA512 -no-emit-version -no-comments -keyserver hkps://keys.openpgp.org -auto-key-locate local,wkd # find a key by mail address (WKD) before asking a keyserver -trust-model tofu+pgp # remember first-seen keys per address, warn on change - -# ~/.gnupg/gpg-agent.conf -default-cache-ttl 3600 # seconds a passphrase stays cached after last use -max-cache-ttl 28800 # hard ceiling -# pinentry-program is set by NixOS (hosts/laptop/default.nix: pinentryPackage = pinentry-gnome3) -``` - -```sh -chmod 700 ~/.gnupg # gpg refuses "unsafe permissions" otherwise -gpg-connect-agent reloadagent /bye # after editing gpg-agent.conf -gpgconf --kill gpg-agent # restart it entirely (it relaunches on demand) -gpg --version # algorithms available -``` - -## keygen — a proper key, the modern way - -Certify-only primary, then three subkeys. Ed25519/Curve25519 throughout (fast, small, the 2.4 default). - -```sh -gpg --quick-generate-key 'DAEMON-SEC <zer0sec.xp@icloud.com>' ed25519 cert 2y # primary: [C] only, 2-year expiry -FPR=$(gpg -K --with-colons 'zer0sec.xp@icloud.com' | awk -F: '$1=="fpr"{print $10; exit}') -gpg --quick-add-key "$FPR" ed25519 sign 1y # [S] -gpg --quick-add-key "$FPR" cv25519 encr 1y # [E] -gpg --quick-add-key "$FPR" ed25519 auth 1y # [A] (for SSH) -gpg -K --with-subkey-fingerprints # see the four, check capabilities and expiries -``` - -- Expiry is a **safety net**, not a deadline: extend it any time with the primary key (see *edit*). A key that expires with its owner gone is self-cleaning. -- Interactive version: `gpg --full-generate-key --expert` → `(11) ECC (set your own capabilities)` → toggle `S` off so only `Certify` remains → `Curve 25519`. Then add subkeys with `--quick-add-key` or in `--edit-key` → `addkey`. -- A passphrase protects the secret key *file*; the agent caches it. Use a long one; the pinentry dialog asks. -- The revocation certificate is written automatically to `~/.gnupg/openpgp-revocs.d/FPR.rev` — back it up with the key (see *backup*). - -## subkeys — add, rotate, drop - -```sh -gpg --quick-add-key FPR ed25519 sign 1y # new signing subkey (old one stays until you revoke it) -gpg --quick-set-expire FPR 1y '*' # extend EVERY subkey a year (`'*'` = all subkeys; a sub-fpr = that one) -gpg --quick-set-expire FPR 2y # extend the primary -gpg --edit-key FPR # interactive: `key 2` selects subkey 2 (the `*` marks it), then - # `expire` / `revkey` / `delkey` / `passwd` / `save` -gpg -K --with-colons | awk -F: '$1=="ssb"{print $12, $5}' # capability letters + key id of each subkey -``` - -Old encryption subkeys: **keep them** (revoked or expired, still in the ring) or you lose the ability to read what was sent to them. - -## backup — export, revocation, paper - -```sh -gpg --armor --export FPR > daemon-sec.pub.asc # public key: share freely -gpg --armor --export-secret-keys FPR > daemon-sec.SECRET.asc # everything: primary + subkeys (passphrase-protected) -gpg --armor --export-secret-subkeys FPR > daemon-sec.subkeys.asc # subkeys only (what a laptop should carry; see *offline*) -cp ~/.gnupg/openpgp-revocs.d/FPR.rev daemon-sec.rev # the revocation certificate -gpg --export-ownertrust > ownertrust.txt # your trust assignments -gpg --gen-revoke FPR > daemon-sec.rev # make a fresh revocation cert by hand -nix shell nixpkgs#paperkey -c sh -c 'gpg --export-secret-keys FPR | paperkey > daemon-sec.paper.txt' # printable secret bits -``` - -Where it goes: the vault (encrypted), never the dotfiles repo. The SECRET export + the .rev are the two files that matter. -The secret export is still encrypted with your passphrase; the passphrase is the third thing to keep. - -## import — keys, trust, restore - -```sh -gpg --import daemon-sec.pub.asc # someone's public key (or your own on a new machine) -gpg --import daemon-sec.SECRET.asc # restore: asks for the passphrase -gpg --import-ownertrust < ownertrust.txt -gpg --edit-key FPR trust # set ownertrust: 5 = ultimate (yours), 4 = full, 3 = marginal -gpg --lsign-key FPR # "I checked this key": local signature, never exported -gpg --sign-key FPR # exportable certification (web of trust) -gpg --show-keys someone.asc # look at a key file WITHOUT importing it -gpg --delete-keys FPR # forget a public key; --delete-secret-keys for the secret part first -``` - -On a fresh machine: import the SECRET file, `gpg --edit-key FPR trust` → `5`, done. Or import only the subkeys file for a laptop. - -## sign — files, text, commits - -```sh -gpg --detach-sign --armor file # file.asc beside it: the usual way to sign a release/file -gpg --detach-sign file # binary file.sig -gpg --clearsign message.txt # message.txt.asc: readable text with a signature block (mail, announcements) -gpg --sign file # file.gpg: compressed file + signature in one (recipient runs gpg -d) -gpg -u FPR --detach-sign file # pick the key (-u / --local-user); `FPR!` = that exact subkey -echo "text" | gpg --clearsign # from a pipe -gpg --sign --encrypt -r mail file # sign AND encrypt (see *encrypt*) -``` - -Signing uses the `[S]` subkey automatically (or the primary if it has S). The agent asks for the passphrase once per cache TTL. - -## verify — did this come from them, unchanged - -```sh -gpg --verify file.asc file # detached signature (.asc/.sig) + the file -gpg --verify file.sig # gpg finds `file` next to it -gpg --verify message.txt.asc # clearsigned text -gpg --decrypt file.gpg > file # inline-signed: extracts the file and prints the verification -gpg --verify --verbose file.asc file # which key, which subkey, when -``` - -Reading the result: **Good signature from "Name <mail>"** = cryptographically valid. The warning -`This key is not certified with a trusted signature` means you have not set ownertrust / signed their key -— the signature is still valid; the question is whether the key is really theirs. Compare the fingerprint -out-of-band once, then `gpg --lsign-key FPR` and the warning goes away. -`BAD signature` = the file changed or the signature is for another file. Exit code 0 only on Good. - -## encrypt — to people, to yourself, with a passphrase - -```sh -gpg --encrypt --recipient mail@example.com --armor file # file.asc, readable only by that key -gpg -e -r mail@example.com -r zer0sec.xp@icloud.com file # several recipients (add yourself to read it later!) -gpg -e file # to yourself (default-recipient-self in gpg.conf) -gpg -se -r mail file # sign + encrypt: they know it is from you -gpg --symmetric --cipher-algo AES256 file # passphrase only, no keys (file.gpg); shares via a channel you trust -gpg -c --armor file # same, armored -gpg -o out.gpg -e -r mail file # choose the output name -tar cz directory | gpg -e -r mail > directory.tgz.gpg # a whole directory, through a pipe -gpg --hidden-recipient mail -e file # do not reveal who it is for (-R) -gpg --encrypt-to FPR # in gpg.conf: ALWAYS add this recipient (yourself) silently -``` - -- `--armor` (`-a`) makes ASCII text you can paste into mail; without it the output is binary (smaller). -- Encrypt uses the recipient's `[E]` subkey. "No public key" = you have not imported theirs; "unusable public key" = theirs expired. -- Symmetric + a strong passphrase is fine for backups and for sending to someone with no key. - -## decrypt — and what to do when it fails - -```sh -gpg --decrypt file.gpg > file # -d: to stdout -gpg -o file -d file.gpg # to a named file -gpg file.gpg # guesses: decrypts (or verifies) and writes `file` -gpg --decrypt-files *.gpg # many at once, each to its name without .gpg -gpg -d file.gpg | tar xz # straight into tar -gpg --list-packets file.gpg # WHO can decrypt this: the key IDs it was encrypted to, the algorithms -``` - -"decryption failed: No secret key" = it was not encrypted to any key you hold (check with `--list-packets`; -compare with `gpg -K`). On a laptop with subkeys only, that is fine as long as the `[E]` subkey is there. - -## edit — identities, passphrase, expiry - -```sh -gpg --quick-add-uid FPR 'DAEMON-SEC <zer0sec.xp@icloud.com>' # add a user ID (fix a name, add an address) -gpg --quick-set-primary-uid FPR 'DAEMON-SEC <zer0sec.xp@icloud.com>' -gpg --quick-revoke-uid FPR 'DAMEON-NIX <zer0sec.xp@icloud.com>' # retire a UID (keys cannot delete published UIDs; revoke them) -gpg --change-passphrase FPR # new passphrase for the secret key -gpg --quick-set-expire FPR 2y # primary expiry; `0` = never -gpg --quick-set-expire FPR 1y '*' # all subkeys -gpg --edit-key FPR # the interactive editor; `help` lists everything: -# uid N / key N select · adduid deluid revuid primary · addkey delkey revkey expire · passwd · trust · save / quit -``` - -Every change to UIDs or subkeys needs the **primary** secret key present (`sec`, not `sec#`). Then re-export the public key: others must re-import it to see the change. - -## revoke — when a key is lost or compromised - -```sh -gpg --import daemon-sec.rev # the stored revocation certificate: marks YOUR key revoked locally -gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish the revocation so others see it -gpg --edit-key FPR → key 2 → revkey → save # revoke ONE subkey (compromised laptop: revoke its subkeys, keep the primary) -gpg --quick-revoke-uid FPR 'uid string' # revoke an identity -``` - -Revoked ≠ deleted: the key stays in the ring so old signatures still verify (as "made with a revoked key") and old mail still decrypts. -If the laptop is lost and you kept the primary offline: revoke its subkeys, make new ones, publish. The identity survives. - -## ssh — the [A] subkey as your SSH key - -```sh -# hosts/laptop/default.nix: programs.gnupg.agent.enableSSHSupport = true; # gpg-agent becomes the ssh agent (SSH_AUTH_SOCK) -gpg -K --with-keygrip # the keygrip of the [A] subkey -echo KEYGRIP >> ~/.gnupg/sshcontrol # tell the agent to serve it -gpg --export-ssh-key FPR # the public key in authorized_keys format -gpg --export-ssh-key FPR >> ~/.ssh/authorized_keys_for_that_host # paste it where you log in -ssh-add -L # the agent now lists it -``` - -Your file key `~/.ssh/id_ed25519` keeps working next to it; ssh tries agent keys first. Use one or the other per host in `~/.ssh/config` (`IdentitiesOnly yes` + `IdentityFile`). - -## git — signed commits and tags - -```sh -git config --global gpg.format openpgp -git config --global user.signingkey 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 # or a subkey fpr with `!` to force it -git config --global commit.gpgsign true # every commit -git config --global tag.gpgSign true -git commit -S -m "msg" # one-off when gpgsign is off -git tag -s v1.0 -m "release" # signed tag; git tag -v v1.0 verifies -git log --show-signature -3 # see who signed what -git verify-commit HEAD -gpg --armor --export FPR | wl-copy # paste into GitHub/GitLab → Settings → GPG keys, so they show "Verified" -``` - -jj: `jj config set --user signing.behavior own` + `signing.backend "gpg"` + `signing.key FPR` signs the commits jj makes. - -## keyservers — publishing and finding keys - -```sh -gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish; keys.openpgp.org mails you to verify the address before it shows the UID -gpg --keyserver hkps://keys.openpgp.org --recv-keys FPR # fetch by fingerprint (the only safe way to fetch) -gpg --locate-keys someone@example.com # WKD: their domain serves the key (auto-key-locate in gpg.conf) -gpg --search-keys 'name' # interactive search; verify the fingerprint with them before trusting -gpg --refresh-keys # pull revocations/expiry updates for every key you hold -``` - -## trust — validity versus ownertrust - -- A signature is **valid** when the key is valid. A key is valid when *you* certified it (`--sign-key` / `--lsign-key`) or enough trusted people did. -- **Ownertrust** (`--edit-key FPR trust`, 1-5) is how much you let that person's certifications count for *other* keys. Give 5 only to yourself. -- `trust-model tofu+pgp`: the first key seen for an address is remembered; a different one later triggers a warning. `gpg --tofu-policy good FPR` to accept a change. -- `gpg --check-signatures FPR` lists who certified a key. `gpg --update-trustdb` recomputes validity after trust changes. - -## inspect — what is this thing - -```sh -gpg -k # public keys (--list-keys); gpg -K = secret keys -gpg -k --with-subkey-fingerprints --with-keygrip FPR -gpg --fingerprint FPR # just the fingerprint, formatted for reading aloud -gpg --list-packets file.gpg # structure of any OpenPGP file: recipients, algorithms, signature dates -gpg --show-keys key.asc # describe a key file without importing -gpg -k --with-colons FPR # machine-readable (pub/sub/uid/fpr records) -gpg --card-status # a YubiKey/OpenPGP card, if one is plugged in -gpg --export FPR | gpg --list-packets | grep -A2 'signature packet' # certifications on your key -``` - -## offline — primary key off the laptop - -The point of subkeys: the laptop carries only `[S] [E] [A]`; the primary (the identity) lives on encrypted offline storage. - -```sh -gpg --armor --export-secret-keys FPR > daemon-sec.SECRET.asc # 1. full backup first (vault, encrypted USB) -gpg --armor --export-secret-subkeys FPR > subkeys.asc # 2. the laptop's share -gpg --delete-secret-keys FPR # 3. remove everything secret here -gpg --import subkeys.asc # 4. put the subkeys back -gpg -K # shows `sec#` = primary absent, `ssb` present: correct -``` - -To add a subkey / extend expiry / sign someone's key later: import the SECRET backup in a temporary `GNUPGHOME`, do the change, export the public key and subkeys again, wipe the temp dir: -```sh -export GNUPGHOME=$(mktemp -d); gpg --import daemon-sec.SECRET.asc; gpg --quick-set-expire FPR 2y '*' -gpg --armor --export FPR > pub.asc; gpg --armor --export-secret-subkeys FPR > subkeys.asc; rm -rf "$GNUPGHOME"; unset GNUPGHOME -gpg --import pub.asc subkeys.asc # back in the normal ring -``` -A YubiKey does the same with the subkeys moved onto the card: `gpg --edit-key FPR` → `key N` → `keytocard`. - -## agent — passphrase caching, pinentry - -```sh -gpg-connect-agent 'KEYINFO --list' /bye # which keys are cached right now (the `1` column) -gpg-connect-agent reloadagent /bye # re-read gpg-agent.conf -gpgconf --kill gpg-agent # forget every cached passphrase now -gpg --pinentry-mode loopback -d file.gpg # type the passphrase in the terminal instead of the dialog (what gpgx does) -echo test | gpg --clearsign >/dev/null # cheap way to pre-unlock the key for a batch of operations -``` - -`export GPG_TTY=$(tty)` must be set for terminal pinentries (core.zsh does it). The dialog here is pinentry-gnome3. - -## fix — the usual errors - -- `Inappropriate ioctl for device` → `export GPG_TTY=$(tty)` (or the session has no tty: use `--pinentry-mode loopback`). -- `No secret key` on decrypt → not encrypted to you: `gpg --list-packets file.gpg` shows the key IDs it was made for. -- `unusable public key` / `unusable secret key` → expired or revoked (sub)key: `gpg -k FPR`, then `--quick-set-expire` (yours) or ask for a fresh key (theirs). -- `There is no assurance this key belongs to the named user` → ownertrust: `gpg --lsign-key FPR` after checking the fingerprint, or `--trust-model always` for one command. -- `gpg: WARNING: unsafe permissions on homedir` → `chmod 700 ~/.gnupg; chmod 600 ~/.gnupg/*`. -- Commands hang → a stale lock or a stuck daemon: `gpgconf --kill all`, `rm -f ~/.gnupg/*.lock ~/.gnupg/public-keys.d/*.lock`. -- `signing failed: No pinentry` → agent cannot reach a pinentry: `gpgconf --kill gpg-agent` and retry from a terminal with `GPG_TTY` set. -- Git: `error: gpg failed to sign the data` → almost always the two above; `GIT_TRACE=1 git commit` shows gpg's own message. -- Key shows `[expired]` on the other side after you extended it → they need your re-exported public key (or `--refresh-keys`). - -## mine — this machine, today - -- **Main key** `3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4`, UID `daemon (Main_Key) <zer0sec.xp@icloud.com>`, - RSA 4096 made 2025-12-30, imported from the vault on 2026-10-08: primary `[SC]` + one `[E]` subkey, **no expiry**, - ownertrust ultimate, `default-key` in gpg.conf, git signs with it (`commit.gpgsign`, `tag.gpgSign` on). - No revocation certificate is stored here yet: `gpg --gen-revoke 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 > daemon-main.rev` - and put it in the vault. Optional upgrades, all without changing the identity: an expiry (`--quick-set-expire`), - a `[S]` signing subkey and an `[A]` auth subkey (`--quick-add-key … rsa4096 sign|auth`), the primary offline (*offline*). -- **Spare** `E989B40F2382569DD6080336BF684D91DF095E48` (`DAMEON-NIX`, ed25519, made 2026-10-08, never published): - delete it with `gpg --delete-secret-and-public-keys E989B40F2382569DD6080336BF684D91DF095E48`, or keep it as a scratch key. -- Pinentry: GNOME dialog (hosts/laptop/default.nix). `gpgx` uses loopback so the passphrase is typed in the terminal instead. -- Secrets in the NixDaemon repo are **age**, not gpg (`nix-cheat secrets`); the vault's `ssd.key.gpg` is gpg-encrypted, decrypt it with `gpg -d`. -- `gpgx-cheat` is the wrapper's card; `gpg-cheat SECTION` prints one section of this one. diff --git a/home/cheats/nix.md b/home/cheats/nix.md @@ -1,238 +0,0 @@ -# nix — rebuilding this machine from ~/NixDaemon - -NixOS **and** home-manager are one flake: `~/NixDaemon` (`NH_FLAKE` points at it). -home-manager runs as a NixOS module, so **one rebuild does both**; there is no separate `home-manager switch` here. -Terminal card: **`nix-cheat`** — sections `layout rebuild remote nh home search update rollback clean inspect add dotfiles secrets repo shell`. - -## layout — what lives where - -```text -~/NixDaemon/flake.nix inputs (nixpkgs unstable, home-manager, hyprland, caelestia) · output nixosConfigurations.nixos -hosts/laptop/default.nix the machine: boot, users (zsh login shell), greetd, Hyprland/uwsm, audio, fonts -hosts/laptop/*.nix fan fix, nvidia, ssd, nix-settings (nh, caches), toolbox (envfs, PATH) -home/default.nix home-manager entry; imports home/modules/*.nix -home/modules/tools.nix packages: the toolbox closure and the general CLI tools ← add packages here -home/modules/shell.nix zsh wiring (ZDOTDIR, fzf, completions), tmux plugins, secretspec -home/modules/dotfiles.nix every dotfile from ~/git/daemon-sec-dotfiles, as symlinks ← add dotfile paths here -home/modules/hyprland.nix Hyprland Lua config + helper scripts (wallpaper-picker, keybinds-menu …) -home/hypr/*.lua core · looknfeel (animations) · defaults (binds) · bindings · lid · caelestia -home/modules/caelestia.nix the shell (bar, launcher, lock), its CLI, wallpaper dir -hosts/laptop/sops.nix sops-nix (system) · home/modules/sops.nix (user) · secrets/secrets.yaml · .sops.yaml -``` - -## rebuild — nixos-rebuild, the plain way - -```sh -cd ~/NixDaemon -sudo nixos-rebuild switch --flake .#nixos # build, activate now, make it the boot default -sudo nixos-rebuild boot --flake .#nixos # build, activate on next boot only (kernel/driver changes) -sudo nixos-rebuild test --flake .#nixos # activate now, NOT the boot default (try something out) -nixos-rebuild build --flake .#nixos # just build → ./result, no sudo, nothing changes -sudo nixos-rebuild switch --flake ~/NixDaemon#nixos # same, from any directory -``` - -- `#nixos` is the configuration name (= hostname). `#bootstrap` is the old Stage A GNOME config. -- **New files must be known to git** or the flake does not see them: `git add path` first (see *repo*). -- A rebuild activates home-manager too; its log: `journalctl --user -u home-manager-daemonsec -b` or `systemctl status home-manager-daemonsec`. - -## remote — build straight from the GitLab repo - -The flake does not have to be checked out: any `nix` command takes a **flake reference** to the repo. -The repo is **private**, so use the ssh form (the key in ~/.ssh/config is registered on GitLab); `?ref=main` pins a branch, `?rev=<sha>` a commit. - -```sh -REPO='git+ssh://git@gitlab.com/DAEMON-404/NixDaemon.git' -sudo nixos-rebuild switch --flake "$REPO#nixos" # this machine, from the pushed main -sudo nixos-rebuild boot --flake "$REPO?ref=main#nixos" -nh os switch "$REPO" # nh takes the same reference -nix build "$REPO#nixosConfigurations.nixos.config.system.build.toplevel" --no-link --print-out-paths -nix flake show "$REPO" # what the repo exports -nix flake metadata "$REPO" # which commit nix resolved -# root (sudo) fetches with root's ssh: either run the fetch as you first (nix build …, cached), or give -# root the key via /root/.ssh/config, or use an https token in ~/.config/nix/netrc (machine gitlab.com …). -``` - -**Fresh machine, from the live ISO** (disk already partitioned and mounted at /mnt): -```sh -sudo nixos-generate-config --root /mnt --show-hardware-config > hardware-configuration.nix # compare with hosts/laptop/ -git clone git@gitlab.com:DAEMON-404/NixDaemon.git ~/NixDaemon && cd ~/NixDaemon # ssh key first (see *secrets*) -# copy the new hardware-configuration.nix into hosts/laptop/, git add it, then: -sudo nixos-install --flake .#nixos -``` -Afterwards: clone `~/git/daemon-sec-dotfiles` (every dotfile links there), restore the age key to -`~/.config/sops/age/keys.txt` and `/var/lib/sops-nix/key.txt` (see *secrets*), and the toolbox to `~/.local/bin`. - -A remote reference is fetched fresh each time (nix caches it briefly, `--refresh` forces it); the local -checkout at `~/NixDaemon` (`NH_FLAKE`) is the default for day-to-day work. - -## nh — the same, with a diff and a progress tree - -`nh` wraps nixos-rebuild: builds with nix-output-monitor, shows an **nvd diff** of what changes, then asks for sudo. - -```sh -nh os switch # = sudo nixos-rebuild switch --flake ~/NixDaemon (NH_FLAKE), config picked by hostname -nh os boot # build + boot default, activate on reboot -nh os test # activate now, not the boot default -nh os build # build only, no activation, no sudo -nh os switch --dry # show what would happen, do nothing -nh os switch --ask # show the diff, then confirm before activating -nh os switch -H bootstrap # another configuration from the same flake (-H = hostname/attr) -nh os switch -u # `nix flake update` first, then switch (updates EVERY input — see *update*) -nh os info # list system generations -nh os rollback # go back one generation (see *rollback*) -nh os switch -- --show-trace # anything after -- goes to nix build (debug an eval error) -``` - -## home — home-manager in this setup - -- home-manager is **inside** the NixOS build (`home-manager.nixosModules.home-manager` in flake.nix, user `daemonsec` → `./home`). - **`nh os switch` rebuilds it**; `nh home switch` / `home-manager switch` do not apply here (no standalone HM profile). -- Did my home change apply? `ls -l ~/.config/<thing>` (links point into /nix/store or the dotfiles checkout), - `systemctl --user status home-manager-daemonsec`, or `journalctl --user -u home-manager-daemonsec -b`. -- HM backs up a file it has to replace as `*.hm-bak` (flake.nix `backupFileExtension`). Delete the backup once happy. -- Only build the home part (fast check, no sudo): - `nix build ~/NixDaemon#nixosConfigurations.nixos.config.home-manager.users.daemonsec.home.activationPackage` - -## search — finding packages and options - -```sh -ns # fuzzy search everything indexed: nixpkgs packages, NixOS and home-manager options -ns kitty # start with a query -``` -Inside `ns` (fzf): type to filter, ↑↓ move, the right pane shows the description, version, homepage and -the option's default. **Enter** prints the attribute name (paste it into `home.packages` in tools.nix), -**ctrl-y** copies it, **ctrl-o** opens the homepage, **ctrl-s** the nixpkgs source, **Esc** quits. -The index is built by `nix-search-tv` on first use (a few seconds) and refreshed by itself; by hand: -```sh -nix-search-tv print | head # the raw list · nix-search-tv preview firefox # one entry -nh search firefox # search.nixos.org from the terminal (needs network) -nix search nixpkgs firefox # nix's own search (slow first run: builds an eval cache) -nix eval ~/NixDaemon#nixosConfigurations.nixos.pkgs.firefox.version # version THIS flake would install -nix shell nixpkgs#firefox # try it without installing · nix run nixpkgs#cowsay -- hi -``` - -## update — moving the inputs - -```sh -cd ~/NixDaemon -nix flake update # all inputs → flake.lock (nixpkgs, home-manager, hyprland, caelestia, llm-agents) -nix flake update nixpkgs home-manager # only these inputs -nix flake update hyprland # Hyprland alone (uses hyprland.cachix.org, so usually no compile) -nix flake lock # (re)write the lock without updating -nix flake metadata # which revisions are locked right now -nh os boot # then build it; boot = safest for kernel/driver bumps -``` - -Hyprland and Caelestia pin each other (flake.nix comments): update `hyprland` and `caelestia-*` together if the shell breaks. - -## rollback — when a generation misbehaves - -```sh -nh os rollback # previous generation, now -sudo nixos-rebuild switch --rollback # the same, plain -nh os info # generation numbers -sudo nix-env --switch-generation 5 -p /nix/var/nix/profiles/system && sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch -``` - -- At power-on the **systemd-boot menu** lists every generation: pick an older one, it boots as it was. -- A broken Hyprland login: Ctrl+Alt+F2 for a console, then one of the above. - -## clean — store and generations - -```sh -nh clean all --keep 5 --keep-since 14d # what the weekly timer runs (nix-settings.nix): drop old generations, GC -nh clean all --dry # show what it would remove -sudo nix-collect-garbage -d # the blunt version: delete all old generations, then GC -nix store gc # GC only (nothing referenced by a generation is touched) -du -sh /nix/store # how big is it -``` - -## inspect — see before you switch - -```sh -nh os build && nvd diff /run/current-system result # what a switch would change -nix build ~/NixDaemon#nixosConfigurations.nixos.config.system.build.toplevel --no-link --print-out-paths -nix eval ~/NixDaemon#nixosConfigurations.nixos.pkgs.uv.version # version a package would get -nix eval ~/NixDaemon#nixosConfigurations.nixos.config.programs.zsh.enable # read an option value -ns <query> # fuzzy search nixpkgs + NixOS/HM options, with descriptions (nix-search-tv) -nh search <package> # nixpkgs search (search.nixos.org) -nix search nixpkgs <package> # local search (first run builds an index) -nix shell nixpkgs#<package> # try a tool without installing it -nix run nixpkgs#<package> -- --help -nix flake check ~/NixDaemon # evaluate every output -nix flake show ~/NixDaemon -``` - -## add — packages, options, dotfiles - -- **A package for the user**: `home/modules/tools.nix` → `home.packages` list → `nh os switch`. -- **A system package / service**: `hosts/laptop/default.nix` (`environment.systemPackages`, `services.*`). -- **A dotfile from the checkout**: `home/modules/dotfiles.nix` → add its path to the list → `nh os switch`. -- **A Hyprland bind**: `home/hypr/bindings.lua` (user) or `defaults.lua` (stock); a helper script goes in `hyprland.nix`. -- **A Caelestia setting**: `home/modules/caelestia.nix` → `programs.caelestia.settings`. -- Then: `git add` anything new, `nh os switch`, and for Hyprland `hyprctl reload`. - -## dotfiles — the checkout is the source of truth - -- `~/git/daemon-sec-dotfiles/home/` is mirrored into `$HOME` by **symlinks** (dotfiles.nix): edit a file there and the live config changes at once — no rebuild. -- A rebuild is needed only to add/remove a *path* in dotfiles.nix. `ls -l ~/.config/starship.toml` shows where a link goes. -- zsh: `~/.dotfiles/config/*.zsh` → open a new shell (`exec zsh`). Plugins are vendored in `~/.dotfiles/config/plugins`. -- Not linked on purpose (hypr, kitty, git, mimeapps, bash rc, .claude …): the list and reasons are at the top of dotfiles.nix. - -## secrets — sops-nix and secretspec - -Two tools, two jobs. **sops-nix**: secrets that belong in this repo, encrypted with **age**, decrypted at -activation to `/run/secrets/NAME` (system) or `~/.config/sops-nix/secrets/NAME` (user). -**secretspec**: runtime secrets for a *project*, declared in its `secretspec.toml`, stored in the keyring. - -```sh -# sops — the one key: ~/.config/sops/age/keys.txt (user) = /var/lib/sops-nix/key.txt (system, root copy) -sudo install -D -m 600 -o root -g root ~/.config/sops/age/keys.txt /var/lib/sops-nix/key.txt # once per machine -age-keygen -y ~/.config/sops/age/keys.txt # the public key (recipient) in .sops.yaml -sops secrets/secrets.yaml # edit: opens decrypted in $EDITOR, re-encrypts on save -sops set secrets/secrets.yaml '["wifi-psk"]' '"hunter2"' # add/replace one value without the editor -sops -d secrets/secrets.yaml # print decrypted -sops -d --extract '["example"]' secrets/secrets.yaml -sops updatekeys secrets/secrets.yaml # after editing the recipients in .sops.yaml -``` - -Then declare it and rebuild: -```nix -# hosts/laptop/sops.nix (system) # home/modules/sops.nix (user) -sops.secrets.wifi-psk = { }; sops.secrets.my-token = { }; -sops.secrets."ssd.key".path = "/etc/secrets/ssd.key"; # e.g. the LUKS key instead of restoring it by hand -``` -`nh os switch` → `/run/secrets/wifi-psk` (root, 0400; `owner = "daemonsec";` to read it as the user). -Check: `sudo ls -l /run/secrets/`, `systemctl --user status sops-nix` (user side), `journalctl -b | grep sops`. - -```sh -# secretspec — in a project directory (config: ~/.config/secretspec/config.toml → keyring, profile default) -secretspec init # writes secretspec.toml (commit it; it holds names, never values) -secretspec add DATABASE_URL # declare a secret (flags: --required/--default/--description) -secretspec check # prompts for every missing value, stores it in the keyring -secretspec set NAME # (re)store one value -secretspec run -- ./server # run with the secrets in the environment -secretspec export # print them for another tool (shell `eval`) -secretspec claude configure # let Claude Code fetch its API credential through secretspec -``` - -## repo — committing ~/NixDaemon - -The repo is **jj, colocated with git** (`.jj` + `.git`). Nix flakes only see files git knows about, and jj keeps git's index at `@-`, so a *new* file is invisible to nix until it is added: - -```sh -cd ~/NixDaemon -git add home/modules/new.nix # make nix see a new file (modified tracked files are seen as-is) -nh os build # or switch -jj status # jj snapshots the working copy -jj describe -m "what changed" && jj new # seal it (the vault ritual) — or plain: git add -A && git commit -jj log # history -``` - -A dirty tree only prints `warning: Git tree '/home/daemonsec/NixDaemon' is dirty`; it still builds. - -## shell — after a switch - -- New packages are on PATH in a **new** shell (`exec zsh`); the login shell itself changes at the next login. -- Hyprland: `hyprctl reload` re-reads `~/.config/hypr/*.lua`. Caelestia: `caelestia shell -r` restarts the shell. -- Everything in the Nix profile: `ls /etc/profiles/per-user/daemonsec/bin`. diff --git a/home/default.nix b/home/default.nix @@ -1,32 +0,0 @@ -# home/default.nix — home-manager for the laptop user (imported from flake.nix). -{ config, pkgs, lib, user, ... }: -{ - imports = [ - ./modules/hyprland.nix # compositor config (Lua) and the carried shortcuts - ./modules/caelestia.nix # programs.caelestia, shell.json, the Rosé Pine scheme - ./modules/terminal.nix # kitty, fonts - ./modules/tools.nix # toolbox runtime closure, python env, dotfiles links - ./modules/shell.nix # zsh via the dotfiles ZDOTDIR tree, tmux plugins, secretspec - ./modules/dotfiles.nix # every dotfile from ~/git/daemon-sec-dotfiles, as out-of-store symlinks - ./modules/cheats.nix # nix-cheat: the rebuild / nh / flake card - ./modules/sops.nix # sops-nix for the user (same secrets file, age key in ~/.config/sops/age) - ./modules/neovim.nix # nvf: Neovim with a small, Nix-built plugin set - ./modules/prompt.nix # starship prompt and fastfetch card, Rosé Pine, NixOS logo - ./modules/fan.nix # `fan`: status/watch without root, max/auto with a clamp watchdog - ./modules/ssh.nix # the ssh key (sops) and ~/.ssh/config - ./modules/gpg.nix # the GPG main key (public in-repo, secret via sops) and gpg.conf - ./modules/git.nix # git identity, signing with the main key, delta - ./modules/media.nix # mpd + rmpc, mpv - ./modules/yazi.nix # yazi with previews, Rosé Pine, plugins - ./modules/gtk.nix # Yaru-purple icons, cursor, prefer-dark - ]; - - home = { - username = user; - homeDirectory = "/home/${user}"; - stateVersion = "26.05"; - }; - - programs.home-manager.enable = true; - xdg.enable = true; -} diff --git a/home/modules/caelestia.nix b/home/modules/caelestia.nix @@ -1,249 +0,0 @@ -# home/modules/caelestia.nix — Caelestia Shell as bar, launcher, notifications, -# OSD, lock and session menu; its CLI; the static Rosé Pine (M3) schemes. -# -# Look (2026-10-08): Rosé Pine dark (main, not moon) everywhere, translucent -# shell layers over blur, and Caelestia's own framed bar: the screen edge is a -# 10 px frame with 25 px rounded inner corners, the clock and tray sit in pill -# backgrounds, occupied workspaces are filled, the Nix snowflake is the logo. -# The sidebar, utilities and notification panels are narrower than stock -# (../caelestia/shell-tokens.json: 340 / 340 / 360 px instead of 430). -# A Rosé Pine Dawn mapping is registered too: caelestia scheme set -n rose-pine -f rose-pine-dawn -# and back: caelestia scheme set -n rose-pine -f rose-pine-dark -# -{ config, pkgs, lib, inputs, ... }: -let - system = pkgs.stdenv.hostPlatform.system; - hyprPkg = inputs.hyprland.packages.${system}.hyprland; - - # The CLI knows schemes by name and re-reads their colours whenever the - # wallpaper changes, so the two hand-mapped Rosé Pine → M3 palettes are - # registered as a real scheme (name rose-pine; flavours rose-pine-dark, mode - # dark, and rose-pine-dawn, mode light). The CLI's own `rosepine/dawn` is a - # Material palette generated from a gold seed, not the Rosé Pine colours. - cli = (inputs.caelestia-cli.packages.${system}.default).overrideAttrs (old: { - patchPhase = (old.patchPhase or "") + '' - install -Dm644 ${../caelestia/rose-pine-dark.txt} src/caelestia/data/schemes/rose-pine/rose-pine-dark/dark.txt - install -Dm644 ${../caelestia/rose-pine-dawn.txt} src/caelestia/data/schemes/rose-pine/rose-pine-dawn/light.txt - ''; - }); - - shell = - ((inputs.caelestia-shell.packages.${system}.with-cli).override { - caelestia-cli = cli; - hyprland = hyprPkg; # hyprctl from the same Hyprland the compositor runs - }).overrideAttrs (old: { - # bar.activeWindow.compact shows the program (desktop-entry name for the - # window class) instead of the window title. Upstream's compact mode only - # trims the title at its last " - ". Rebuilds the shell locally. - patches = (old.patches or [ ]) ++ [ ../caelestia/active-window-program-name.patch ]; - }); - - wallpaperDir = "${config.home.homeDirectory}/git/daemon-sec-dotfiles/home/.config/omarchy/backgrounds/rose-pine-dark"; -in -{ - programs.caelestia = { - enable = true; - package = shell; - - cli = { - enable = true; - package = cli; - # Theming is static Rosé Pine from this repo; the CLI must not rewrite - # kitty, GTK, Hyprland or anything else when the wallpaper changes. - settings.theme = { - enableTerm = false; - enableHypr = false; - enableDiscord = false; - enableSpicetify = false; - enablePandora = false; - enableFuzzel = false; - enableBtop = false; - enableNvtop = false; - enableHtop = false; - enableGtk = false; - enableQt = false; - enableWarp = false; - enableChromium = false; - enableZed = false; - enableCava = false; - }; - }; - - settings = { - appearance = { - font = { - clock = "Rubik"; - workspaces = "Rubik"; - headline.family = "Noto Sans"; - title.family = "Noto Sans"; - body.family = "Noto Sans"; - label.family = "Noto Sans"; - mono.family = "DMMono Nerd Font"; - }; - anim.durations.scale = 1.15; - transparency = { - enabled = true; - base = 0.85; - layers = 0.4; - }; - }; - general = { - # The Nix snowflake in the bar, dashboard and lock screen (empty = Caelestia's own logo). - logo = "/run/current-system/sw/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; - apps = { - terminal = [ "kitty" ]; - audio = [ "caelestia" "shell" "drawers" "toggle" "sidebar" ]; # was the Omarchy audio popup - playback = [ "mpv" ]; - explorer = [ "nautilus" ]; - }; - idle = { - lockBeforeSleep = false; - inhibitWhenAudio = true; - # Lock after 15 min idle, screen off after 20; audio playing or a - # fullscreen app with an idle inhibitor holds both off. - timeouts = [ - { timeout = 900; idleAction = "lock"; respectInhibitors = true; } - { timeout = 1200; idleAction = "dpms off"; returnAction = "dpms on"; } - ]; - }; - }; - # Omarchy drew the wallpaper; here Caelestia does. Colours stay static. - background = { - enabled = true; - wallpaperEnabled = true; - # Big clock on the wallpaper, bottom right, with a soft shadow. - desktopClock = { - enabled = true; - position = "bottom-right"; - scale = 1.0; - shadow.enabled = true; - }; - # Audio visualiser along the bottom of the wallpaper while something plays (cava is built in). - visualiser = { - enabled = true; - autoHide = true; - blur = false; - rounding = 1; - spacing = 1; - }; - }; - bar = { - persistent = true; - showOnHover = true; - workspaces = { - shown = 5; - activeIndicator = true; - occupiedBg = true; # filled pills behind workspaces that have windows - showWindows = true; - activeTrail = true; - }; - activeWindow = { - compact = true; # the program's name (patched, see `shell` above), not the title - inverted = true; # on a primary-coloured pill - }; - clock = { - showDate = true; - showIcon = true; - background = true; # clock in its own pill - }; - tray = { - background = true; # tray in its own pill - recolour = true; # tray icons tinted to the scheme - }; - statusIcons = [ - { id = "lockStatus"; enabled = true; } - { id = "audio"; enabled = true; } - { id = "microphone"; enabled = true; } # shows when something is capturing - { id = "kbLayout"; enabled = false; } - { id = "network"; enabled = true; } - { id = "bluetooth"; enabled = true; } - { id = "battery"; enabled = true; } - ]; - }; - # Caelestia's frame: the bar is one side of a border around the screen - # whose inner corners are rounded. Stock values; the carried 5/0/0 was a flat strip. - border = { - thickness = 10; - rounding = 25; - smoothing = 20; - }; - dashboard = { - enabled = true; - showWeather = true; - performance.showGpu = true; - }; - launcher = { - enabled = true; - maxShown = 8; - vimKeybinds = true; # ctrl+j/k move, like everywhere else here - actions = [ - { name = "Calculator"; icon = "calculate"; description = "Do simple math equations (powered by Qalc)"; command = [ "autocomplete" "calc" ]; enabled = true; dangerous = false; } - { name = "Wallpaper"; icon = "image"; description = "Pick a wallpaper"; command = [ "caelestia" "wallpaper" ]; enabled = true; dangerous = false; } - { name = "Lock"; icon = "lock"; description = "Lock the session"; command = [ "caelestia" "shell" "lock" "lock" ]; enabled = true; dangerous = false; } - { name = "Sleep"; icon = "bedtime"; description = "Suspend"; command = [ "systemctl" "suspend" ]; enabled = true; dangerous = false; } - { name = "Settings"; icon = "settings"; description = "Configure the shell"; command = [ "caelestia" "shell" "nexus" "open" ]; enabled = true; dangerous = false; } - { name = "Logout"; icon = "exit_to_app"; description = "Log out of the current session"; command = [ "uwsm" "stop" ]; enabled = true; dangerous = true; } - { name = "Reboot"; icon = "cached"; description = "Reboot the system"; command = [ "systemctl" "reboot" ]; enabled = true; dangerous = true; } - { name = "Shutdown"; icon = "power_settings_new"; description = "Shutdown the system"; command = [ "systemctl" "poweroff" ]; enabled = true; dangerous = true; } - ]; - }; - lock = { - enabled = true; # Caelestia is the lock screen here (Omarchy had hyprlock) - useWallpaper = true; # the wallpaper behind the lock, not a flat colour - }; - notifs = { - expire = true; - defaultExpireTimeout = 6000; - actionOnClick = true; - }; - osd = { - enabled = true; - enableBrightness = true; - }; - services = { - gpuType = "Generic"; # must be a string - smartScheme = false; # never derive colours from the wallpaper - defaultPlayer = "rmpc"; - weatherLocation = "54.15,-4.48"; # Douglas, Isle of Man (dashboard weather card) - }; - session = { - enabled = true; - icons.hibernate = "bedtime"; - commands = { - logout = [ "uwsm" "stop" ]; - shutdown = [ "systemctl" "poweroff" ]; - hibernate = [ "systemctl" "suspend" ]; - reboot = [ "systemctl" "reboot" ]; - }; - }; - sidebar.enabled = true; - utilities = { - enabled = true; - toasts.nowPlaying = true; # a toast when the track changes - }; - paths.wallpaperDir = wallpaperDir; - }; - }; - - # The scheme the shell reads at start: Rosé Pine dark (scheme.json; - # scheme-dawn.json is the light mapping). `caelestia scheme set …` rewrites - # this file (home-manager backs the symlink up as .hm-bak when that happens). - home.file.".local/state/caelestia/scheme.json".source = ../caelestia/scheme.json; - - # Internal size tokens: the shell reads this file (defaults in its source, - # plugin/src/Caelestia/Config/tokens.hpp: sidebar, utilities and - # notification width 430, bar innerWidth 40). Here: sidebar and utilities - # 340, notifications 360, bar 36. Rounding, padding and spacing stay stock. - home.file.".config/caelestia/shell-tokens.json".source = ../caelestia/shell-tokens.json; - - # First wallpaper, only if none was ever chosen (Caelestia keeps the choice in state). - home.activation.caelestiaWallpaper = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - st="$HOME/.local/state/caelestia/wallpaper" - if [ ! -e "$st/path.txt" ]; then - wp=$(ls "${wallpaperDir}"/*.png 2>/dev/null | head -1 || true) - if [ -n "$wp" ]; then - mkdir -p "$st" && printf '%s' "$wp" > "$st/path.txt" && ln -sfn "$wp" "$st/current" - fi - fi - ''; -} diff --git a/home/modules/cheats.nix b/home/modules/cheats.nix @@ -1,61 +0,0 @@ -# home/modules/cheats.nix — the cheat cards this repo ships, in the house -# style (~/.local/bin/*-cheat, rendered with the toolbox's cheat-render). -# Every home/cheats/<name>.md becomes a `<name>-cheat` command: -# -# nix-cheat rebuilding this machine: nixos-rebuild, nh, remote, search, update, rollback, secrets, repo -# gpg-cheat GnuPG from the key hierarchy to signing, encryption, SSH, git, offline primary, fixes -# -# <name>-cheat the whole card <name>-cheat --list the section names -# <name>-cheat SECTION one section <name>-cheat --raw the markdown itself -# -# Rendered with cheat-render when that is on PATH, else glow, else printed -# plain. These cards live here (not in the dotfiles) because they document -# this repo and this machine; xcheats only lists ~/.local/bin cards, so call -# these by name. -{ pkgs, lib, ... }: -let - cards = [ "nix" "gpg" ]; - - mkCheat = name: pkgs.writeShellScriptBin "${name}-cheat" '' - set -uo pipefail - card=${../cheats + "/${name}.md"} - - usage() { - echo "usage: ${name}-cheat [SECTION] [--list] [--raw] [--help]" - echo " sections: $(sections | tr '\n' ' ')" - } - sections() { # first word of each '## ' heading - ${pkgs.gnused}/bin/sed -n 's/^## \([a-z-]*\).*/\1/p' "$card" - } - section() { # the heading whose first word matches $1, up to the next heading - ${pkgs.gawk}/bin/awk -v want="$1" ' - /^## / { on = (tolower($2) == want) } - /^# / { next } - on - ' "$card" - } - render() { - if [ ! -t 1 ]; then cat - elif command -v cheat-render >/dev/null 2>&1; then cheat-render - | ''${PAGER:-less -R} - else ${pkgs.glow}/bin/glow -p - - fi - } - - case "''${1:-}" in - -h|--help) usage; exit 0 ;; - --list) sections; exit 0 ;; - --raw) cat "$card"; exit 0 ;; - "") render < "$card" ;; - *) - key=$(echo "$1" | tr '[:upper:]' '[:lower:]') - out=$(section "$key") - if [ -z "$out" ]; then - echo "${name}-cheat: no section '$1'" >&2; usage >&2; exit 1 - fi - { head -1 "$card"; echo; printf '%s\n' "$out"; } | render ;; - esac - ''; -in -{ - home.packages = map mkCheat cards; -} diff --git a/home/modules/dotfiles.nix b/home/modules/dotfiles.nix @@ -1,114 +0,0 @@ -# home/modules/dotfiles.nix — every dotfile from the dotfiles checkout, placed -# by home-manager. -# -# ~/git/daemon-sec-dotfiles is the restorable snapshot of the Omarchy -# workstation (its README: "files restored relative to $HOME"). home-manager -# puts each of its files where it belongs as an out-of-store symlink -# (mkOutOfStoreSymlink), so there is one source of truth: edit the checkout and -# the live config changes; `nh os switch` is only needed when a path is added -# or removed here. The links dangle harmlessly until the repo is cloned. -# -# Not linked, and why (each is one line to add if wanted): -# .config/hypr, .config/kitty Nix-managed (home/modules/hyprland.nix, terminal.nix) -# .config/nvim the AstroNvim tree; Neovim is nvf now (home/modules/neovim.nix) -# .config/starship.toml, .config/fastfetch the Omarchy-era prompt and fetch; both are -# Nix-managed now (home/modules/prompt.nix) -# .config/mpd, rmpc, mpv, yazi Nix-managed (home/modules/media.nix, yazi.nix), carried from the checkout -# .config/omarchy*, Omacom, hyprmoncfg Omarchy's own trees; caelestia.nix reads the -# wallpaper directory straight from the checkout -# .config/systemd/user Omarchy-era units; caelestia-shell.service there would -# fight the home-manager caelestia service -# .config/autostart Omarchy autostarts for apps not installed here -# .config/mimeapps.list defaults point at chromium / HEY, neither installed: -# xdg-open would fail on every link -# .config/git turns on commit signing with a key not on this machine -# .config/fontconfig, dconf, gtk-4.0 home-manager writes these (fonts.fontconfig, gtk.nix) -# .config/gtk-3.0/bookmarks paths under the old /home/daemon-sec -# .config/user-dirs.dirs, floorp XDG defaults already match; a browser profile is state -# .config/tmux holds only a disabled backup; ~/.tmux.conf is the config -# .bashrc, .bash_profile, .bash_logout source Omarchy's bash rc unguarded (errors on NixOS) -# .zshrc, .zprofile the dead decoys; ZDOTDIR=~/.dotfiles bypasses them -# .XCompose includes /usr/share/omarchy/default/xcompose -# .claude, .codex, .agents live agent state on this machine (plugins, sessions) -# bin, .local/bin ~/.local/bin is the live toolbox repo (README) -# .local/share/applications Omarchy web-app launchers (omarchy-launch-webapp) -# .local/share/icons/hicolor apps install into it; the themes are linked one by one -{ config, lib, ... }: -let - repo = "${config.home.homeDirectory}/git/daemon-sec-dotfiles"; - home = "${repo}/home"; - link = path: config.lib.file.mkOutOfStoreSymlink "${home}/${path}"; - - # Same path under $HOME as in the checkout's home/. - same = paths: lib.genAttrs paths (p: { source = link p; }); - # Entries of .config, by name. - config' = names: lib.genAttrs names (n: { source = link ".config/${n}"; }); - - cursorThemes = [ - "modernxp-retro-black" # the active cursor (gtk.nix, core.lua) - "modernxp-retro-black-hyprcursor" - "modernxp-rose-pine" - "modernxp-rose-pine-hyprcursor" - "retrosmart-rose-pine" - "retrosmart-rose-pine-hyprcursor" - "rose-pine-hyprcursor" - "BreezeX-RosePine-Linux" - ]; -in -{ - home.file = - same [ - ".dotfiles" # the zsh ZDOTDIR tree (home/modules/shell.nix sets ZDOTDIR) - ".tmux.conf" - ".ripgreprc" # RIPGREP_CONFIG_PATH, exported by .dotfiles/config/ripgrep.zsh - "Music/AGENTS.md" - ] - // same (map (t: ".local/share/icons/${t}") cursorThemes) - // { - # The patched DMMono TTFs live outside home/ in the checkout. - ".local/share/fonts/nerd-fonts-dm-mono".source = - config.lib.file.mkOutOfStoreSymlink "${repo}/assets/fonts/nerd-fonts-dm-mono"; - }; - - xdg.configFile = config' [ - # shell and prompt - "atuin" - "bat" # the "Rose Pine" theme BAT_THEME names (shell.nix rebuilds bat's cache) - "carapace" - "cheats" # the markdown cheat cards (cheats.zsh, ~/.local/bin/cheat-*) - "crossfetch" # the login splash animation (animations.zsh); the fetch card itself is prompt.nix - "eza" - "mise" - # tools - "btop" - "lazygit" - "jj" - "emacs" - "opencode" - "feroxbuster" - "uncover" - "herdr" - "tensaku" - "ai-usagebar" - "bg-pasticcio" - "libvirt" - # media - "cava" - "imv" - "zathura" - "xournalpp" - "spicetify" - "vesktop" - "pipewire" # 10-sample-rates.conf - "wireplumber" # bluetooth-a2dp-autoconnect.conf - # terminals and desktop bits - "alacritty" - "foot" - "ghostty" - "fcitx5" - "xdg-terminals.list" # kitty first, for xdg-terminal-exec - "chromium-flags.conf" # read only if a chromium is ever installed - "menus" # the chrome-apps application menu entries - "uwsm" # env.d/50-rose-pine-cursor (same values core.lua sets) - ]; -} diff --git a/home/modules/fan.nix b/home/modules/fan.nix @@ -1,120 +0,0 @@ -# home/modules/fan.nix — `fan`: the laptop's fans from the terminal, safely. -# -# fan one line: CPU °C, fan rpm and %, clock cap, load, clamp yes/no, EC mode -# fan watch [s] the same line every s seconds (default 2), Ctrl-C to stop -# fan max 100 % now, with the watchdog (below) fan 60 fixed 60 % (30-100) -# fan auto back to the EC's own curve; stops the watchdog -# fan log what the watchdog has done -# -# Why a watchdog: this laptop's EC, once in manual fan mode, asserts PROCHOT -# and pins every core at 399 MHz as soon as load starts (fan-throttle-guard.nix). -# `fan max` therefore starts a transient user unit (fan-watchdog) that samples -# /proc/stat and /proc/cpuinfo every second and, the moment CPU busy ≥ 25 % -# while no core is above 600 MHz (fanfix's clamp rule), runs `fan-ec auto`, -# sends a notification and exits. Root access is `sudo -n fan-ec …` -# (hosts/laptop/fan-cli.nix: a store script with a passwordless rule for wheel). -# Reads need no root at all: k10temp and the uniwill hwmon are world-readable. -{ pkgs, lib, ... }: -let - bin = lib.makeBinPath [ pkgs.coreutils pkgs.gawk pkgs.gnugrep pkgs.gnused pkgs.systemd pkgs.libnotify ]; - # NixOS's setuid sudo lives in /run/wrappers; the store copy is not setuid - # and refuses to run ("must be owned by uid 0 and have the setuid bit set"). - sudo = "/run/wrappers/bin/sudo"; - - # shared read-only sampler, sourced by both scripts - lib-sh = pkgs.writeText "fan-lib.sh" '' - TRIP_MHZ=600; BUSY_MIN=25 - STATE=''${XDG_RUNTIME_DIR:-/tmp}/fan.stat - hw() { local n; for h in /sys/class/hwmon/hwmon*; do n=$(cat "$h/name" 2>/dev/null); [ "$n" = "$1" ] && { echo "$h"; return; }; done; } - cpu_temp() { local h; h=$(hw k10temp); [ -n "$h" ] && echo $(( $(cat "$h/temp1_input") / 1000 )) || echo -; } - fan_rpm() { local h; h=$(hw uniwill); [ -n "$h" ] && cat "$h/fan1_input" || echo -; } - fan_pct() { local h; h=$(hw uniwill); [ -n "$h" ] && echo $(( $(cat "$h/pwm1") * 100 / 255 )) || echo -; } - gpu_temp() { local h; h=$(hw uniwill); [ -n "$h" ] && echo $(( $(cat "$h/temp2_input") / 1000 )) || echo -; } - cap_mhz() { echo $(( $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq) / 1000 )); } - clocks() { awk '/^cpu MHz/ {s+=$4; n++; if ($4>m) m=$4} END {printf "%d %d", (n?s/n:0), m}' /proc/cpuinfo; } - # CPU busy % since the previous call (snapshot in $STATE); 0 on the first call - busy() { - local cur prev b=0 - cur=$(head -1 /proc/stat) - [ -r "$STATE" ] && prev=$(cat "$STATE") || prev= - printf '%s' "$cur" > "$STATE" - [ -n "$prev" ] && b=$(awk -v a="$prev" -v b="$cur" 'BEGIN { - na=split(a,x," "); nb=split(b,y," "); ta=0; tb=0 - for (i=2;i<=na;i++) ta+=x[i]; for (i=2;i<=nb;i++) tb+=y[i] - ia=x[5]+x[6]; ib=y[5]+y[6]; d=tb-ta; if (d<=0) {print 0; exit} - printf "%d", 100*(d-(ib-ia))/d }') - echo "$b" - } - clamped() { # 1 when busy yet no core above TRIP_MHZ - local b=$1 mx=$2; [ "$b" -ge "$BUSY_MIN" ] && [ "$mx" -gt 0 ] && [ "$mx" -lt "$TRIP_MHZ" ] && echo 1 || echo 0 - } - ec_mode() { ${sudo} -n /run/current-system/sw/bin/fan-ec mode 2>/dev/null || echo "?"; } - status_line() { - local b mx avg; b=$(busy); read -r avg mx <<< "$(clocks)" - local cl; cl=$(clamped "$b" "$mx") - printf 'CPU %s°C · GPU %s°C · fan %s rpm %s%% · cap %s MHz · clocks avg %s / max %s · load %s%% · clamp %s · fans %s\n' \ - "$(cpu_temp)" "$(gpu_temp)" "$(fan_rpm)" "$(fan_pct)" "$(cap_mhz)" "$avg" "$mx" "$b" \ - "$([ "$cl" = 1 ] && echo YES || echo no)" "$(ec_mode)" - } - ''; - - fan-watchdog = pkgs.writeShellScriptBin "fan-watchdog" '' - set -uo pipefail - PATH=${bin}:$PATH - . ${lib-sh} - LOG=''${XDG_STATE_HOME:-$HOME/.local/state}/fan-watchdog.log; mkdir -p "$(dirname "$LOG")" - log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$LOG"; } - log "armed: fans manual ($1), watching for the EC clamp" - busy >/dev/null; sleep 1 - while :; do - b=$(busy); read -r _ mx <<< "$(clocks)" - if [ "$(clamped "$b" "$mx")" = 1 ]; then - out=$(${sudo} -n /run/current-system/sw/bin/fan-ec auto 2>&1) - log "CLAMP: load $b %, max core $mx MHz -> fan-ec auto: $out" - notify-send -a fan -u critical "Fans back to auto" "EC clamp hit (load $b %, cores at $mx MHz). Manual fan mode released." - exit 0 - fi - sleep 1 - done - ''; - - fan = pkgs.writeShellScriptBin "fan" '' - set -uo pipefail - PATH=${bin}:$PATH - . ${lib-sh} - UNIT=fan-watchdog - EC=/run/current-system/sw/bin/fan-ec - LOG=''${XDG_STATE_HOME:-$HOME/.local/state}/fan-watchdog.log - - arm() { # start (or restart) the watchdog as a transient user unit - systemctl --user stop "$UNIT" 2>/dev/null || true - systemd-run --user --unit="$UNIT" --collect --quiet ${fan-watchdog}/bin/fan-watchdog "$1" \ - && echo "watchdog armed: back to auto the moment the EC clamps the CPU (fan log)" - } - disarm() { systemctl --user stop "$UNIT" 2>/dev/null || true; } - - if [ "$(id -u)" = 0 ]; then - echo "fan: run this as yourself, not under sudo (it needs your user session for the watchdog; root access is handled inside)" >&2 - exit 1 - fi - - case "''${1:-}" in - ""|status) status_line ;; - watch) - iv=''${2:-2}; busy >/dev/null; sleep "$iv" - while :; do status_line; sleep "$iv"; done ;; - max) ${sudo} -n "$EC" max && arm max ;; - auto) disarm; ${sudo} -n "$EC" auto ;; - [0-9]*) p=''${1%\%}; ${sudo} -n "$EC" "$p" && arm "$p %" ;; - ec) ${sudo} -n "$EC" status ;; - log) [ -r "$LOG" ] && tail -n ''${2:-20} "$LOG" || echo "no watchdog log yet" ;; - -h|--help|help) - echo "usage: fan [status] | watch [sec] | max | <30-100> | auto | ec | log [n]" - echo " max / <pct> put the fans in manual mode WITH the clamp watchdog; auto releases them." ;; - *) echo "fan: unknown command '$1' (try: fan help)" >&2; exit 2 ;; - esac - ''; -in -{ - home.packages = [ fan fan-watchdog ]; -} diff --git a/home/modules/git.nix b/home/modules/git.nix @@ -1,81 +0,0 @@ -# home/modules/git.nix — git, from the flake (was ~/.gitconfig written by the -# bootstrap script plus the dotfiles' .config/git, which is not linked). -# -# Identity: DAEMON-404 <zer0sec.xp@icloud.com>; every commit and tag signed -# with the GPG main key (home/modules/gpg.nix), which GitLab already knows. -# Credentials for https remotes come from gh / glab; clones use ssh anyway. -# delta paints diffs (Rosé Pine, from the dotfiles' git config). -{ ... }: -{ - programs.git = { - enable = true; - signing = { - key = "3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4"; - format = "openpgp"; - signByDefault = true; # commit.gpgSign and tag.gpgSign - }; - settings = { - user = { - name = "DAEMON-404"; - email = "zer0sec.xp@icloud.com"; - }; - alias = { - co = "checkout"; - br = "branch"; - ci = "commit"; - st = "status"; - lg = "log --oneline --graph --decorate -20"; - }; - init.defaultBranch = "main"; - pull.rebase = true; - push.autoSetupRemote = true; - diff = { - algorithm = "histogram"; - colorMoved = "default"; - mnemonicPrefix = true; - }; - commit.verbose = true; - column.ui = "auto"; - branch.sort = "-committerdate"; - tag.sort = "-version:refname"; - rerere = { - enabled = true; - autoupdate = true; - }; - merge.conflictstyle = "zdiff3"; - core = { - autocrlf = "input"; - safecrlf = "warn"; - }; - credential = { - "https://github.com".helper = "!gh auth git-credential"; - "https://gist.github.com".helper = "!gh auth git-credential"; - "https://gitlab.com".helper = "!glab auth git-credential"; - }; - }; - }; - - programs.delta = { - enable = true; - enableGitIntegration = true; - options = { - navigate = true; - light = false; - line-numbers = true; - side-by-side = false; - hyperlinks = true; - syntax-theme = "none"; - minus-style = "\"#eb6f92\" \"#26233a\""; - minus-emph-style = "bold \"#eb6f92\" \"#403d52\""; - plus-style = "\"#9ccfd8\" \"#26233a\""; - plus-emph-style = "bold \"#31748f\" \"#403d52\""; - line-numbers-minus-style = "\"#eb6f92\""; - line-numbers-plus-style = "\"#31748f\""; - line-numbers-zero-style = "\"#6e6a86\""; - file-style = "\"#31748f\" bold"; - file-decoration-style = "\"#c4a7e7\" ul"; - hunk-header-style = "\"#eb6f92\" bold"; - hunk-header-decoration-style = "\"#6e6a86\" box"; - }; - }; -} diff --git a/home/modules/gpg.nix b/home/modules/gpg.nix @@ -1,51 +0,0 @@ -# home/modules/gpg.nix — the GPG main key and gpg.conf, from the flake. -# -# public key home/gpg/daemon-main.pub.asc → imported with ultimate trust (programs.gpg.publicKeys) -# secret key sops secret gpg_main_secret (the armored export, still under its own passphrase): -# imported into the keyring on activation if the keyring lacks it -# gpg.conf programs.gpg.settings (the gpg-cheat `setup` defaults) -# -# The agent itself is NixOS's (programs.gnupg.agent, pinentry-gnome3), so -# services.gpg-agent is deliberately not enabled here. -# Key: daemon (Main_Key) <zer0sec.xp@icloud.com>, RSA 4096, 2025-12-30, the one on GitLab. -{ config, pkgs, lib, ... }: -let - fpr = "3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4"; -in -{ - sops.secrets.gpg_main_secret = { }; - - programs.gpg = { - enable = true; - mutableKeys = true; # other people's keys and new subkeys stay editable - mutableTrust = true; - publicKeys = [ - { source = ../gpg/daemon-main.pub.asc; trust = 5; } - ]; - settings = { - default-key = fpr; - default-recipient-self = true; - keyid-format = "0xlong"; - with-fingerprint = true; - with-subkey-fingerprints = true; - personal-cipher-preferences = "AES256 AES192 AES"; - personal-digest-preferences = "SHA512 SHA384 SHA256"; - cert-digest-algo = "SHA512"; - no-emit-version = true; - no-comments = true; - keyserver = "hkps://keys.openpgp.org"; - auto-key-locate = "local,wkd"; - trust-model = "tofu+pgp"; - }; - }; - - # Import the secret key once (idempotent: skipped when the keyring has it). - # Runs after sops-nix has decrypted the secrets. - home.activation.gpgMainKey = lib.hm.dag.entryAfter [ "sops-nix" ] '' - if [ -r "${config.sops.secrets.gpg_main_secret.path}" ] \ - && ! ${pkgs.gnupg}/bin/gpg --batch --list-secret-keys ${fpr} >/dev/null 2>&1; then - run ${pkgs.gnupg}/bin/gpg --batch --quiet --import "${config.sops.secrets.gpg_main_secret.path}" \ - && echo "gpg: imported the main secret key ${fpr}" - fi - ''; -} diff --git a/home/modules/gtk.nix b/home/modules/gtk.nix @@ -1,26 +0,0 @@ -# home/modules/gtk.nix — icons, cursor, dark preference. -# The cursor theme files are symlinked from the dotfiles checkout in tools.nix -# (modernxp-retro-black for Xcursor, modernxp-retro-black-hyprcursor for -# Hyprland), so there is no package to point home.pointerCursor at. -{ pkgs, ... }: -{ - gtk = { - enable = true; - iconTheme = { - name = "Yaru-purple"; - package = pkgs.yaru-theme; - }; - cursorTheme = { - name = "modernxp-retro-black"; - size = 24; - }; - colorScheme = "dark"; # GTK prefer-dark - }; - - home.sessionVariables = { - XCURSOR_THEME = "modernxp-retro-black"; - XCURSOR_SIZE = "24"; - HYPRCURSOR_THEME = "modernxp-retro-black-hyprcursor"; # name from the theme's manifest.hl - HYPRCURSOR_SIZE = "24"; - }; -} diff --git a/home/modules/hyprland.nix b/home/modules/hyprland.nix @@ -1,149 +0,0 @@ -# home/modules/hyprland.nix — Hyprland (Lua config) and the carried shortcuts. -# -# Hyprland 0.56 is configured in Lua (hyprland.lua, `hl.*` API); that is also -# the dialect the carried shortcuts and the toolbox helpers (dropterm, winsnap, -# vault-open, lid-control: `hyprctl dispatch 'hl.dsp…'`) already speak. The -# config is split like the vault's shortcuts/: -# hypr/omarchy.lua the `o` helpers the carried files were written against -# hypr/core.lua monitor, env, look, input, Caelestia layer rules -# hypr/looknfeel.lua the springy animations, shadows, snap, swallow (dotfiles looknfeel.lua) -# hypr/defaults.lua the stock Omarchy binds as captured in keybinds.txt -# hypr/bindings.lua shortcuts/bindings.lua (user overrides, window mode) -# hypr/lid.lua shortcuts/lid.lua -# hypr/caelestia.lua shortcuts/caelestia.lua (Caelestia keys, always on here) -{ config, pkgs, lib, inputs, ... }: -let - system = pkgs.stdenv.hostPlatform.system; - hyprPkg = inputs.hyprland.packages.${system}.hyprland; - shellCli = "${config.programs.caelestia.cli.package}/bin/caelestia"; - - # Small helpers the stock Omarchy binds relied on. They exist only to serve - # this config, so they live here rather than in ~/.local/bin. - helpers = [ - # The picker the omarchy-menu-* cheat sheets (bin/) pipe into. Prints the chosen line. - (pkgs.writeShellScriptBin "omarchy-menu-select" '' - title=''${1:-Select} - exec ${pkgs.fuzzel}/bin/fuzzel --dmenu --prompt "$title › " --width 110 --lines 24 - '') - # omarchy-not-ported "<bind description>" ["<what it did on Omarchy>"]: an - # honest notification instead of a key that silently does nothing. - (pkgs.writeShellScriptBin "omarchy-not-ported" '' - ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 4000 "$1" "Not set up on this NixOS build.''${2:+ Omarchy: $2}" - '') - (pkgs.writeShellScriptBin "nixdaemon-show" '' - # nixdaemon-show time|battery|calendar|kbd-backlight-cycle - n() { ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 5000 "$@"; } - case "''${1:-}" in - time) n "$(date '+%H:%M')" "$(date '+%A %-d %B %Y')" ;; - battery) b=/sys/class/power_supply/BAT0; n "Battery $(cat $b/capacity)%" "$(cat $b/status)" ;; - calendar) n "$(date '+%B %Y')" "$(cal | tail -n +2)" ;; - kbd-backlight-cycle) - d=$(ls -d /sys/class/leds/*kbd_backlight 2>/dev/null | head -1); [ -n "$d" ] || exit 0 - max=$(cat "$d/max_brightness"); cur=$(cat "$d/brightness"); next=$(( (cur + 1) % (max + 1) )) - ${pkgs.brightnessctl}/bin/brightnessctl -q -d "$(basename "$d")" set "$next" ;; - *) echo "usage: nixdaemon-show time|battery|calendar|kbd-backlight-cycle" >&2; exit 2 ;; - esac - '') - # SUPER+K: searchable list of the live binds (what Omarchy's keybindings menu did). Enter copies the key. - (pkgs.writeShellScriptBin "keybinds-menu" '' - sel=$(hyprctl binds -j | ${pkgs.python3}/bin/python3 -I -c ' - import json, sys - M = {1: "SHIFT", 4: "CTRL", 8: "ALT", 64: "SUPER"} - rows = set() - for x in json.load(sys.stdin): - mods = "+".join(n for v, n in sorted(M.items()) if x["modmask"] & v) - key = x["key"] or ("code:%d" % x["keycode"]) - sub = x.get("submap", "") - rows.add(("%s%s%s %s" % (sub + ": " if sub else "", mods + " + " if mods else "", key, x.get("description", ""))).rstrip()) - print("\n".join(sorted(rows)))' | ${pkgs.fuzzel}/bin/fuzzel --dmenu --prompt "Keybindings › " --width 120 --lines 30) || exit 0 - [ -n "$sel" ] && printf '%s' "''${sel%% *}" | ${pkgs.wl-clipboard}/bin/wl-copy - '') - # CTRL+SUPER+SPACE: what Omarchy's background switcher did, with Caelestia's - # own wallpaper grid. The launcher shows it when its search starts with - # ">wallpaper ", and there is no IPC for that, so the prefix is typed in. - (pkgs.writeShellScriptBin "wallpaper-picker" '' - c=${shellCli} - case "$($c shell drawers isOpen launcher 2>/dev/null)" in - 1|true) exec $c shell drawers toggle launcher ;; - esac - $c shell drawers toggle launcher - sleep 0.25 - exec ${pkgs.wtype}/bin/wtype '>wallpaper ' - '') - (pkgs.writeShellScriptBin "nightlight-toggle" '' - if pgrep -x hyprsunset >/dev/null; then - pkill -x hyprsunset; ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 2000 "Night light" "off" - else - ${pkgs.hyprsunset}/bin/hyprsunset --temperature 4000 >/dev/null 2>&1 & - ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 2000 "Night light" "on (4000 K)" - fi - '') - ]; -in -{ - wayland.windowManager.hyprland = { - enable = true; - package = hyprPkg; # same derivation NixOS installs; no second copy - portalPackage = null; # programs.hyprland (NixOS) provides the portal - configType = "lua"; - systemd.enable = false; # uwsm owns graphical-session.target - xwayland.enable = true; - - extraLuaFiles = { - omarchy = { content = ../hypr/omarchy.lua; autoLoad = false; }; - core = { content = ../hypr/core.lua; autoLoad = false; }; - looknfeel = { content = ../hypr/looknfeel.lua; autoLoad = false; }; - defaults = { content = ../hypr/defaults.lua; autoLoad = false; }; - bindings = { content = ../hypr/bindings.lua; autoLoad = false; }; - lid = { content = ../hypr/lid.lua; autoLoad = false; }; - caelestia = { content = ../hypr/caelestia.lua; autoLoad = false; }; - }; - - # Explicit load order: look first, then the stock binds, then the carried files that - # unbind-and-rebind the keys they take over, Caelestia's keys last. - extraConfig = '' - local cfg = (os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME") .. "/.config")) .. "/hypr" - package.path = cfg .. "/?.lua;" .. package.path - require("omarchy") - require("core") - require("looknfeel") - require("defaults") - require("bindings") - require("lid") - require("caelestia") - ''; - }; - - home.packages = helpers ++ (with pkgs; [ - hyprpicker # colour picker (SUPER+PRINT) - hyprsunset # night light - fuzzel # dmenu for the cheat sheets and keybinds-menu - nautilus # the file manager shell.json and the stock binds point at - brightnessctl - pamixer - grim - slurp - wl-clipboard - libnotify - ]); - - # Session services under graphical-session.target (started by uwsm). - systemd.user.services.hyprpolkitagent = { - Unit = { - Description = "Hyprland polkit authentication agent"; - After = [ "graphical-session.target" ]; - PartOf = [ "graphical-session.target" ]; - }; - Service = { - ExecStart = "${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent"; - Restart = "on-failure"; - Slice = "session.slice"; - }; - Install.WantedBy = [ "graphical-session.target" ]; - }; - services.cliphist.enable = true; # history for `caelestia clipboard` - services.udiskie = { - enable = true; - tray = "auto"; - }; -} diff --git a/home/modules/media.nix b/home/modules/media.nix @@ -1,115 +0,0 @@ -# home/modules/media.nix — music and video: mpd + rmpc, and mpv. -# -# mpd runs as a user service (starts at login), library ~/Music (the layout -# contract is ~/Music/AGENTS.md), state in ~/.local/share/mpd, PipeWire -# output plus the FIFO rmpc's visualiser reads. It listens on the socket -# $XDG_RUNTIME_DIR/mpd/socket and on 127.0.0.1:6600. -# -# rmpc: the dotfiles' full config (home/rmpc/config.ron: tabs with album art, -# lyrics and cava panes, vim keys, 1-0 tab switching), the Rosé Pine theme -# (home/rmpc/themes/rose-pine.ron) and the LRCLIB lyrics fetcher that runs on -# song change. The Discord presence script was not carried (1.4k lines of -# Python with its own deps; say so if wanted). -# -# mpv: the dotfiles' gpu-next/Vulkan profile with the CfL chroma shader, plus -# uosc (the on-screen UI), thumbfast (seek thumbnails) and mpris (media keys, -# Caelestia's player widget). -{ config, pkgs, ... }: -let - rt = "/run/user/1000"; -in -{ - services.mpd = { - enable = true; - musicDirectory = "${config.home.homeDirectory}/Music"; - playlistDirectory = "${config.xdg.dataHome}/mpd/playlists"; - network = { - listenAddress = "${rt}/mpd/socket"; - port = 6600; - startWhenNeeded = false; - }; - extraConfig = '' - bind_to_address "127.0.0.1" - auto_update "yes" - restore_paused "yes" - audio_output { - type "pipewire" - name "PipeWire" - } - audio_output { - type "fifo" - name "Visualizer FIFO" - path "${rt}/mpd/fifo" - format "44100:16:2" - } - ''; - }; - # the socket's directory, created by systemd before mpd starts - systemd.user.services.mpd.Service.RuntimeDirectory = "mpd"; - - programs.rmpc = { - enable = true; - config = builtins.readFile ../rmpc/config.ron; - }; - xdg.configFile = { - "rmpc/themes/rose-pine.ron".source = ../rmpc/themes/rose-pine.ron; - "rmpc/scripts/fetch-lyrics" = { - source = ../rmpc/scripts/fetch-lyrics; - executable = true; - }; - "mpv/shaders".source = ../mpv/shaders; - }; - - programs.mpv = { - enable = true; - scripts = with pkgs.mpvScripts; [ uosc thumbfast mpris ]; - config = { - vo = "gpu-next"; - gpu-api = "vulkan"; - gpu-context = "waylandvk"; - profile = "high-quality"; - hwdec = "auto"; - scale = "ewa_lanczos4sharpest"; - glsl-shader = "~~/shaders/CfL_Prediction.glsl"; - cscale = "ewa_lanczossharp"; - cscale-antiring = 0.65; - dscale = "mitchell"; - correct-downscaling = true; - linear-downscaling = true; - sigmoid-upscaling = true; - deband = true; - deband-iterations = 2; - deband-threshold = 32; - deband-range = 12; - deband-grain = 16; - dither = "error-diffusion"; - error-diffusion = "burkes"; - dither-depth = 8; - temporal-dither = false; - video-sync = "display-resample"; - interpolation = true; - tscale = "oversample"; - target-colorspace-hint = "auto"; - target-colorspace-hint-mode = "target"; - target-prim = "bt.709"; - target-trc = "srgb"; - gamut-mapping-mode = "perceptual"; - tone-mapping = "auto"; - hdr-compute-peak = true; - contrast = 4; - saturation = 5; - screenshot-format = "png"; - screenshot-high-bit-depth = true; - screenshot-tag-colorspace = true; - screenshot-directory = "~/Pictures/mpv"; - osc = false; # uosc replaces it - border = false; - save-position-on-quit = true; - keep-open = true; - sub-auto = "fuzzy"; - slang = "en,eng"; - alang = "ja,jpn,en,eng"; - ytdl-format = "bestvideo[height<=?1440]+bestaudio/best"; - }; - }; -} diff --git a/home/modules/neovim.nix b/home/modules/neovim.nix @@ -1,115 +0,0 @@ -# home/modules/neovim.nix — Neovim through nvf (github:notashelf/nvf): the -# editor and its plugins are one Nix-built package, no plugin manager, no -# ~/.config/nvim, nothing downloaded on first start. Replaces the AstroNvim -# tree the dotfiles carried (2026-10-08): that one pulled ~60 plugins through -# lazy.nvim and compiled treesitter parsers on the machine. -# -# Kept deliberately small. Languages: the ones this machine edits (Nix, Lua -# for Hyprland, Python and Bash for the toolbox, Markdown for the vault, and -# the config formats). Each gets treesitter, an LSP and a formatter; format on -# save is off, `<leader>lf` formats on demand. -# -# <leader>ff / fg / fb telescope: files / live grep / buffers -# - oil: edit the parent directory as a buffer -# <leader>e oil in a floating window -# gd gr K <leader>ca LSP: definition, references, hover, code action (nvf defaults) -# <leader>lf format buffer -# ]c [c <leader>gp gitsigns: next/previous hunk, preview hunk -# gcc gc{motion} comment.nvim -# <Esc> clear search highlight -# <space> leader -# -# kitty's copy mode (home/modules/terminal.nix) starts plain pkgs.neovim with -# -u, so it is unaffected by this configuration and stays instant. -{ inputs, pkgs, ... }: -{ - imports = [ inputs.nvf.homeManagerModules.default ]; - - programs.nvf = { - enable = true; - settings.vim = { - viAlias = true; - vimAlias = true; - - theme = { - enable = true; - name = "rose-pine"; - style = "main"; # main, not moon - transparent = false; - }; - - # Editor behaviour - lineNumberMode = "relNumber"; - searchCase = "smart"; - preventJunkFiles = true; - undoFile.enable = true; - clipboard = { - enable = true; - registers = "unnamedplus"; - providers.wl-copy.enable = true; - }; - options = { - tabstop = 2; - shiftwidth = 2; - softtabstop = 2; - scrolloff = 6; - wrap = false; - signcolumn = "yes"; - cursorline = true; - splitbelow = true; - splitright = true; - updatetime = 250; - timeoutlen = 400; - }; - - # Languages: treesitter + LSP + formatter each, chosen by nvf's defaults - # (nil for Nix, basedpyright/ruff for Python, lua-language-server, - # bash-language-server/shfmt, marksman, yaml/json/taplo). - lsp = { - enable = true; - formatOnSave = false; - inlayHints.enable = false; - }; - languages = { - enableTreesitter = true; - enableFormat = true; - nix = { - enable = true; - format.type = [ "nixfmt" ]; # the style this repo is written in - }; - lua.enable = true; - python.enable = true; - bash.enable = true; - markdown.enable = true; - yaml.enable = true; - json.enable = true; - toml.enable = true; - }; - - autocomplete.blink-cmp.enable = true; - telescope.enable = true; - git.gitsigns.enable = true; - binds.whichKey.enable = true; - statusline.lualine.enable = true; - autopairs.nvim-autopairs.enable = true; - comments.comment-nvim.enable = true; - utility.oil-nvim.enable = true; - visuals.nvim-web-devicons.enable = true; - ui.borders.enable = true; - - keymaps = [ - { key = "-"; mode = "n"; action = "<cmd>Oil<CR>"; desc = "Open parent directory"; silent = true; } - { key = "<leader>e"; mode = "n"; action = "<cmd>Oil --float<CR>"; desc = "Explorer (oil)"; silent = true; } - { key = "<Esc>"; mode = "n"; action = "<cmd>nohlsearch<CR>"; desc = "Clear search highlight"; silent = true; } - { key = "<leader>w"; mode = "n"; action = "<cmd>write<CR>"; desc = "Save"; silent = true; } - { key = "<leader>q"; mode = "n"; action = "<cmd>quit<CR>"; desc = "Quit"; silent = true; } - { key = "<C-h>"; mode = "n"; action = "<C-w>h"; desc = "Window left"; } - { key = "<C-j>"; mode = "n"; action = "<C-w>j"; desc = "Window down"; } - { key = "<C-k>"; mode = "n"; action = "<C-w>k"; desc = "Window up"; } - { key = "<C-l>"; mode = "n"; action = "<C-w>l"; desc = "Window right"; } - { key = "<"; mode = "v"; action = "<gv"; desc = "Dedent, keep selection"; } - { key = ">"; mode = "v"; action = ">gv"; desc = "Indent, keep selection"; } - ]; - }; - }; -} diff --git a/home/modules/prompt.nix b/home/modules/prompt.nix @@ -1,245 +0,0 @@ -# home/modules/prompt.nix — the starship prompt and the fastfetch card, fresh -# (2026-10-08), Rosé Pine, one colour per section, nothing Omarchy. -# -# Prompt (two lines, the dotfiles' "filigree" frame kept, the per-letter -# gradients gone): -# -# ╭╌ ☧ daemonsec@nixos ┄ 󰉋 ~/NixDaemon ┄ main [+2 !1] ⌁⡇⡆· (right: 󰔚 3s · 󰥔 14:02) -# ╰╌ ❯ -# -# glyph iris · user rose · host foam · directory gold · git love (status subtle, -# week heartbeat iris) · languages text · duration/jobs gold · clock rose · -# prompt char foam (love after an error). pine is never ink (3.3:1 on base). -# $CROSS_GLYPH comes from the dotfiles' animations.zsh (☧ + the NixOS glyph). -# -# theme.zsh in the dotfiles runs `starship init zsh` and adds the transient -# prompt, so home-manager's own shell integration stays off here. -# -# fastfetch: the builtin NixOS logo in iris/foam, keys in rotating Rosé Pine -# colours, the modules that matter on this laptop. The login splash -# (animations.zsh) runs plain `fastfetch` when CROSS_FETCH=plain, which -# .dotfiles/.zshrc now sets, so this config draws the whole card. -{ lib, ... }: -let - # Rosé Pine (main) - rp = { - love = "#eb6f92"; - gold = "#f6c177"; - rose = "#ebbcba"; - pine = "#31748f"; - foam = "#9ccfd8"; - iris = "#c4a7e7"; - text = "#e0def4"; - subtle = "#908caa"; - muted = "#6e6a86"; - }; - # the same colours as SGR parameters for fastfetch - sgr = { - love = "38;2;235;111;146"; - gold = "38;2;246;193;119"; - rose = "38;2;235;188;186"; - foam = "38;2;156;207;216"; - iris = "38;2;196;167;231"; - text = "38;2;224;222;244"; - subtle = "38;2;144;140;170"; - muted = "38;2;110;106;134"; - }; - lang = symbol: colour: { - inherit symbol; - format = " [$symbol($version)](fg:${colour})"; - }; -in -{ - programs.starship = { - enable = true; - enableZshIntegration = false; # theme.zsh does it (with the transient prompt) - enableBashIntegration = false; - settings = { - "$schema" = "https://starship.rs/config-schema.json"; - add_newline = true; - palette = "rose_pine"; - palettes.rose_pine = rp; - - format = lib.concatStrings [ - "[╭╌](fg:muted) " - "\${env_var.CROSS_GLYPH}" - "$username" - "$hostname" - "$shlvl" - "$sudo" - "\${custom.root}" - "$directory" - "$git_branch" - "$git_status" - "\${custom.gitweek}" - "$git_state" - "$python" - "$nodejs" - "$rust" - "$golang" - "$lua" - "$docker_context" - "$package" - "$line_break" - "[╰╌](fg:muted) " - "$status" - "$character" - ]; - right_format = lib.concatStrings [ "$cmd_duration" "$jobs" "$battery" "$time" ]; - - # identity - env_var.CROSS_GLYPH = { - variable = "CROSS_GLYPH"; - default = "☧"; - format = "[$env_value](bold fg:iris) "; - }; - username = { - show_always = true; - format = "[$user](bold fg:rose)"; - style_user = "bold fg:rose"; - style_root = "bold fg:love"; - }; - hostname = { - ssh_only = false; - format = "[@](fg:muted)[$hostname](bold fg:foam)"; - }; - shlvl = { - disabled = false; - threshold = 2; - format = " [↕$shlvl](bold fg:gold)"; - }; - sudo = { - disabled = false; - format = " [](bold fg:love)"; - }; - custom.root = { - command = "echo ROOT"; - when = "[ \"$(id -u)\" -eq 0 ]"; - format = " [ $output](bold underline fg:love)"; - }; - - # place - directory = { - format = " [┄](fg:muted) [󰉋 $path](bold fg:gold)[$read_only](fg:love)"; - truncation_length = 4; - truncate_to_repo = true; - truncation_symbol = "…/"; - read_only = " 󰌾"; - }; - - # git - git_branch = { - symbol = " "; - format = " [┄](fg:muted) [$symbol$branch(:$remote_branch)](bold fg:love)"; - }; - git_status = { - format = "( [\\[$all_status$ahead_behind\\]](fg:subtle))"; - ahead = "⇡\${count}"; - behind = "⇣\${count}"; - diverged = "⇕⇡\${ahead_count}⇣\${behind_count}"; - conflicted = "="; - untracked = "?"; - stashed = "≡"; - modified = "!"; - staged = "+"; - renamed = "»"; - deleted = "✘"; - }; - # the last 7 days of commits as a braille pulse (carried from the dotfiles) - custom.gitweek = { - command = ''git log --since=7.days --date=format:%Y%m%d --pretty=%cd 2>/dev/null | sort | uniq -c | awk 'BEGIN{split("· ⡀ ⡄ ⡆ ⡇",b," ")}{c[$2]=$1}END{for(i=6;i>=0;i--){d=strftime("%Y%m%d",systime()-i*86400);v=c[d]+0;idx=(v==0)?1:(v<3)?2:(v<6)?3:(v<10)?4:5;printf "%s",b[idx]}}' ''; - when = "git rev-parse --is-inside-work-tree 2>/dev/null | grep -q true"; - format = " [⌁$output](fg:iris)"; - }; - git_state = { - format = " [\\($state $progress_current/$progress_total\\)](bold fg:love)"; - }; - - # toolchains: only when the directory uses them - python = (lang " " "text") // { format = " [$symbol$version( \\($virtualenv\\))](fg:text)"; }; - nodejs = lang " " "text"; - rust = lang " " "text"; - golang = lang " " "text"; - lua = lang " " "text"; - docker_context = { symbol = " "; format = " [$symbol$context](fg:subtle)"; }; - package = { symbol = "󰏗 "; format = " [$symbol$version](fg:subtle)"; }; - - # right side - cmd_duration = { min_time = 2000; format = "[󰔚 $duration](fg:gold) "; }; - jobs = { symbol = "󰜎 "; format = "[$symbol$number](bold fg:gold) "; }; - battery = { - full_symbol = "󱈑 "; - charging_symbol = "󰂄 "; - discharging_symbol = "󱈏 "; - unknown_symbol = "󰂑 "; - empty_symbol = "󰁺 "; - format = "[$symbol$percentage]($style) "; - display = [ - { threshold = 20; style = "bold fg:love"; } - { threshold = 50; style = "fg:gold"; } - ]; - }; - time = { - disabled = false; - time_format = "%H:%M"; - format = "[󰥔 $time](fg:rose)"; - }; - - # second line - status = { - disabled = false; - symbol = "✗ "; - format = "[$symbol$status](fg:love) "; - }; - character = { - success_symbol = "[❯](bold fg:foam)"; - error_symbol = "[❯](bold fg:love)"; - vimcmd_symbol = "[❮](bold fg:gold)"; - vimcmd_replace_one_symbol = "[❮](bold fg:rose)"; - vimcmd_replace_symbol = "[❮](bold fg:iris)"; - vimcmd_visual_symbol = "[❮](bold fg:gold)"; - }; - line_break.disabled = false; - }; - }; - - programs.fastfetch = { - enable = true; - settings = { - "$schema" = "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json"; - logo = { - type = "builtin"; - source = "nixos"; - color = { "1" = sgr.iris; "2" = sgr.foam; }; - padding = { top = 1; left = 2; right = 5; }; - }; - display = { - separator = " "; - color = { keys = sgr.foam; title = sgr.rose; }; - }; - modules = [ - { type = "title"; color = { user = sgr.rose; at = sgr.muted; host = sgr.foam; }; } - { type = "separator"; string = "┄"; length = 34; outputColor = sgr.muted; } - { type = "os"; key = " os"; keyColor = sgr.iris; } - { type = "kernel"; key = " kernel"; keyColor = sgr.foam; } - { type = "uptime"; key = " uptime"; keyColor = sgr.gold; } - { type = "packages"; key = "󰏗 packages"; keyColor = sgr.rose; } - { type = "shell"; key = " shell"; keyColor = sgr.love; } - "break" - { type = "wm"; key = " wm"; keyColor = sgr.iris; } - { type = "display"; key = "󱄄 display"; keyColor = sgr.foam; compactType = "original-with-refresh-rate"; } - { type = "terminal"; key = " terminal"; keyColor = sgr.gold; } - { type = "terminalfont"; key = " font"; keyColor = sgr.rose; } - "break" - { type = "host"; key = "󰌢 host"; keyColor = sgr.love; } - { type = "cpu"; key = " cpu"; keyColor = sgr.iris; showPeCoreCount = true; } - { type = "gpu"; key = " gpu"; keyColor = sgr.foam; detectionMethod = "pci"; format = "{name}"; } - { type = "memory"; key = " memory"; keyColor = sgr.gold; } - { type = "disk"; key = "󰋊 disk"; keyColor = sgr.rose; folders = "/"; } - { type = "battery"; key = "󰁹 battery"; keyColor = sgr.love; } - "break" - { type = "colors"; symbol = "circle"; paddingLeft = 2; } - ]; - }; - }; -} diff --git a/home/modules/shell.nix b/home/modules/shell.nix @@ -1,115 +0,0 @@ -# home/modules/shell.nix — zsh as the shell, configured by the dotfiles. -# -# The dotfiles checkout (~/git/daemon-sec-dotfiles) carries a complete zsh -# setup in home/.dotfiles: a ZDOTDIR tree with core.zsh (history, cached -# compinit, the plugin loader, zoxide/atuin/fzf hooks), the leaf modules -# (aliases, fzf verbs, ripgrep, cheats, music, jj, …), theme.zsh (starship -# with a transient prompt) and animations.zsh (the login splash). The plugins -# it loads — zsh-autosuggestions, zsh-syntax-highlighting, zsh-completions, -# fzf-tab, history-substring-search, you-should-use — are vendored in -# .dotfiles/config/plugins, so the config is used as-is rather than rewritten -# as home-manager options. This module only supplies what the Omarchy install -# had and NixOS does not: -# -# ~/.zshenv sets ZDOTDIR (home-manager owns this one file) -# ~/.dotfiles → the checkout's .dotfiles (edits follow the repo) -# ~/.fzf.zsh fzf's key bindings from the Nix store (core.zsh looks -# in /usr/share/fzf, which does not exist here) -# ~/.zsh/completions generated completions for tools without shipped ones -# tmux plugins from nixpkgs (~/.tmux.conf expects TPM; a shim loads them) -# ~/.config/secretspec the keyring provider -# -# The dotfiles themselves (~/.dotfiles, ~/.tmux.conf, the tool configs under -# ~/.config: starship, bat theme, atuin, fastfetch, cheats, …) are linked by -# home/modules/dotfiles.nix. NixOS side (hosts/laptop/default.nix): programs.zsh with the global compinit -# and prompt off (core.zsh and theme.zsh do those), users.<user>.shell. -{ config, pkgs, lib, ... }: -let - # Completions for tools that do not ship their own under share/zsh. - # (uv, jj, gh, atuin, zoxide, eza, bat, fd, rg do; /etc/zshrc puts the - # profiles' site-functions on fpath before core.zsh runs compinit.) - generatedCompletions = pkgs.runCommand "nixdaemon-zsh-completions" { } '' - mkdir -p $out - export HOME=$TMPDIR - ${pkgs.secretspec}/bin/secretspec completions zsh > $out/_secretspec - ''; - - # ~/.tmux.conf ends with `run '~/.tmux/plugins/tpm/tpm'` and lists plugins - # for TPM to clone. nixpkgs has the plugins but not TPM, so each plugin is - # linked where TPM would have put it and this stand-in sources them. - tpmShim = '' - #!${pkgs.bash}/bin/bash - # Stand-in for tmux-plugin-manager (NixDaemon home/modules/shell.nix): the - # plugins come from nixpkgs and are linked into ~/.tmux/plugins; this runs - # each plugin's entry script the way TPM would. prefix+I/U do nothing here; - # add plugins in shell.nix instead. - for f in "$HOME"/.tmux/plugins/*/*.tmux; do - case "$f" in */tpm/*) continue ;; esac - [ -x "$f" ] && "$f" - done - exit 0 - ''; - tmuxPlugin = name: pkg: { - name = ".tmux/plugins/${name}"; - value.source = "${pkg}/share/tmux-plugins/${pkg.pluginName}"; - }; -in -{ - home.packages = with pkgs; [ - zsh - tmux - secretspec - ]; - - home.file = { - # zsh: hand over to the dotfiles' ZDOTDIR tree. - ".zshenv".text = '' - # Managed by home-manager (NixDaemon home/modules/shell.nix). The shell - # configuration lives in ~/.dotfiles (→ ~/git/daemon-sec-dotfiles). - export ZDOTDIR="$HOME/.dotfiles" - [[ -r "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env" - ''; - ".fzf.zsh".text = '' - # fzf key bindings (Ctrl-T, Alt-C, Ctrl-R) and completion from the Nix - # store; sourced by ~/.dotfiles/config/core.zsh. Quiet when there is no - # tty (the scripts restore `zle`, which fails outside a terminal). - if [[ -t 0 ]]; then - source ${pkgs.fzf}/share/fzf/key-bindings.zsh - source ${pkgs.fzf}/share/fzf/completion.zsh - else - { source ${pkgs.fzf}/share/fzf/key-bindings.zsh; source ${pkgs.fzf}/share/fzf/completion.zsh; } 2>/dev/null - fi - ''; - ".zsh/completions".source = generatedCompletions; - - ".tmux/plugins/tpm/tpm" = { - text = tpmShim; - executable = true; - }; - } - // builtins.listToAttrs [ - (tmuxPlugin "tmux-resurrect" pkgs.tmuxPlugins.resurrect) - (tmuxPlugin "tmux-continuum" pkgs.tmuxPlugins.continuum) - (tmuxPlugin "tmux-yank" pkgs.tmuxPlugins.yank) - (tmuxPlugin "tmux-open" pkgs.tmuxPlugins.open) - ]; - - xdg.configFile = { - # secretspec (https://secretspec.dev): secrets in the system keyring, which - # gnome-keyring provides and PAM unlocks at login. Per-project - # secretspec.toml files declare what a project needs; `secretspec check` - # prompts for anything missing, `secretspec run -- cmd` injects them. - "secretspec/config.toml".text = '' - [defaults] - provider = "keyring" - profile = "default" - ''; - }; - - # bat only sees a theme after its cache is rebuilt from ~/.config/bat/themes. - home.activation.batCache = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if [ -d "$HOME/.config/bat/themes/" ]; then - run ${pkgs.bat}/bin/bat cache --build >/dev/null 2>&1 || true - fi - ''; -} diff --git a/home/modules/sops.nix b/home/modules/sops.nix @@ -1,43 +0,0 @@ -# home/modules/sops.nix — sops-nix for the user: the same encrypted file, -# decrypted into $XDG_RUNTIME_DIR/secrets.d (symlinked at ~/.config/sops-nix/secrets) -# by a user service at login, readable only by daemonsec. -# -# Use this for secrets that belong to the user's programs (API tokens an app -# reads from a file, an rclone config, …); use hosts/laptop/sops.nix for -# anything a system service needs. -# -# sops.secrets.example = { }; # → ~/.config/sops-nix/secrets/example -# sops.secrets.rclone = { path = "${config.xdg.configHome}/rclone/rclone.conf"; }; -# -# secretspec (home/modules/shell.nix) is the complement: per-project runtime -# secrets pulled from the keyring at `secretspec run`, declared next to the -# project in secretspec.toml, not in this repo. -{ config, inputs, pkgs, lib, ... }: -{ - imports = [ inputs.sops-nix.homeManagerModules.sops ]; - - sops = { - defaultSopsFile = ../../secrets/secrets.yaml; - age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt"; - age.sshKeyPaths = [ ]; - gnupg.sshKeyPaths = [ ]; - }; - - home.packages = with pkgs; [ - sops - age - ssh-to-age # turn an ssh-ed25519 key into an age recipient when adding another machine - ]; - - # sops-nix's activation step restarts the sops-nix user unit. That unit is a - # home-manager file (~/.config/systemd/user/sops-nix.service), linked by the - # linkGeneration step, and the user systemd manager only sees new unit - # files after a daemon-reload, which home-manager runs at the very end. With - # the extra steps this config adds, the DAG happened to order sops-nix - # before linkGeneration, so the first switch died with "Unit - # sops-nix.service not found". This entry pins the order: linkGeneration → - # daemon-reload → sops-nix. - home.activation.reloadUserUnitsForSops = lib.hm.dag.entryBetween [ "sops-nix" ] [ "linkGeneration" "installPackages" ] '' - ${pkgs.systemd}/bin/systemctl --user daemon-reload 2>/dev/null || true - ''; -} diff --git a/home/modules/ssh.nix b/home/modules/ssh.nix @@ -1,46 +0,0 @@ -# home/modules/ssh.nix — the SSH key and client config, from the flake. -# -# The private key is a sops secret (secrets/secrets.yaml: ssh_id_ed25519; -# `nix-cheat secrets`). At login sops-nix decrypts it with the age key into -# the runtime secrets dir and ~/.config/sops-nix/secrets/ssh_id_ed25519 points -# there; ~/.ssh/config names that path, so a fresh machine has a working key -# as soon as ~/.config/sops/age/keys.txt is restored. The public half is -# plain text in ~/.ssh/id_ed25519.pub (comment daemonsec@nixos; registered on -# gitlab.com as "nixos", auth and signing, and glab's git_protocol is ssh). -# -# A plain copy from before this module may still sit at ~/.ssh/id_ed25519; -# nothing reads it any more. -{ config, ... }: -let - key = config.sops.secrets.ssh_id_ed25519.path; -in -{ - sops.secrets.ssh_id_ed25519 = { }; # → %r/secrets.d/…, mode 0400 (ssh is happy with that) - - home.file.".ssh/id_ed25519.pub".text = '' - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAsXOt8jkVBgL2ANFgkftVfRlswxBvBUM33Tv/bS+I5Z daemonsec@nixos - ''; - - programs.ssh = { - enable = true; - enableDefaultConfig = false; - settings = { - "gitlab.com" = { - User = "git"; - IdentityFile = key; - IdentitiesOnly = "yes"; - }; - "github.com" = { - User = "git"; - IdentityFile = key; - IdentitiesOnly = "yes"; - }; - "*" = { - IdentityFile = key; - AddKeysToAgent = "yes"; - ServerAliveInterval = 30; - HashKnownHosts = "no"; - }; - }; - }; -} diff --git a/home/modules/terminal.nix b/home/modules/terminal.nix @@ -1,159 +0,0 @@ -# home/modules/terminal.nix — kitty with DMMono Nerd Font and Rosé Pine (main). -# -# The six patched DMMono TTFs come from the dotfiles checkout (tools.nix links -# ~/git/daemon-sec-dotfiles/assets/fonts/nerd-fonts-dm-mono into -# ~/.local/share/fonts). The family has no Bold, so bold maps to Medium. -# -# Palette rule from the migration prompt: pine #31748f is a fill, never ink, -# so it must not sit in an ANSI foreground slot. The Rosé Pine terminal theme -# puts pine in the green slot; the substitute is PARKED for the owner's -# decision. Until then `ansiGreen` below carries foam, the colour the toolbox -# itself uses wherever pine would have been read as text (bin/install.sh). -# -# tmux-style keys (2026-10-08): ctrl+a is a prefix, like tmux's, with tabs as -# tmux windows and kitty windows as tmux panes: -# -# ctrl+a c new tab (cwd kept) ctrl+a - split below (pane) -# ctrl+a n / p next / previous tab ctrl+a | split right -# ctrl+a 1..9 tab N ctrl+a h j k l focus pane left/down/up/right -# ctrl+a , rename tab ctrl+a H J K L move pane -# ctrl+a & close tab ctrl+a o next pane -# ctrl+a x close pane ctrl+a z zoom pane (stack layout toggle) -# ctrl+a [ copy mode (scrollback in Neovim: v y, Enter, q) -# ctrl+a ] paste clipboard ctrl+a space next layout -# ctrl+a { } swap pane back / forth ctrl+a r reload kitty.conf -# ctrl+a u pick a URL (hints) ctrl+a f pick a path (hints) -# ctrl+a ctrl+a send a real ctrl+a to the shell (beginning-of-line) -# ctrl+a shift+arrows resize pane ctrl+a = reset pane sizes -# -# Copy mode is home/kitty/scrollback.lua: the scrollback opens in a bare -# Neovim (no AstroNvim config) with colours, vi motions and search; y copies -# to the clipboard, Enter copies and leaves, q or Esc leaves. -{ pkgs, lib, ... }: -let - ansiGreen = "#9ccfd8"; # PARKED: ask before changing; see header - - # kitty substitutes INPUT_LINE_NUMBER, CURSOR_LINE and CURSOR_COLUMN in the - # pager command; the Lua reads them from vim.g. - scrollbackPager = lib.concatStringsSep " " [ - "${pkgs.bash}/bin/bash -c" - "'exec ${pkgs.neovim}/bin/nvim 63<&0 0</dev/null" - "-u ${../kitty/scrollback.lua}" - "-c \"let g:kitty_input_line=INPUT_LINE_NUMBER\"" - "-c \"let g:kitty_cursor_line=CURSOR_LINE\"" - "-c \"let g:kitty_cursor_col=CURSOR_COLUMN\"'" - ]; - - prefix = "ctrl+a"; - tabKeys = lib.listToAttrs (map (n: { - name = "${prefix}>${toString n}"; - value = "goto_tab ${toString n}"; - }) (lib.range 1 9)); -in -{ - fonts.fontconfig.enable = true; - - programs.kitty = { - enable = true; - font = { - name = "DMMono Nerd Font"; - size = 10; - }; - settings = { - bold_font = ''family="DMMono Nerd Font" style="Medium"''; - italic_font = ''family="DMMono Nerd Font" style="Italic"''; - bold_italic_font = ''family="DMMono Nerd Font" style="Medium Italic"''; - - # Rosé Pine, main (dark) - foreground = "#e0def4"; - background = "#191724"; - selection_foreground = "#e0def4"; - selection_background = "#403d52"; # highlight med - cursor = "#524f67"; # highlight high - cursor_text_color = "#e0def4"; - url_color = "#c4a7e7"; - - active_border_color = "#eb6f92"; - inactive_border_color = "#6e6a86"; - active_tab_foreground = "#e0def4"; - active_tab_background = "#26233a"; - inactive_tab_foreground = "#6e6a86"; - inactive_tab_background = "#191724"; - - color0 = "#26233a"; - color8 = "#6e6a86"; - color1 = "#eb6f92"; - color9 = "#eb6f92"; - color2 = ansiGreen; - color10 = ansiGreen; - color3 = "#f6c177"; - color11 = "#f6c177"; - color4 = "#9ccfd8"; - color12 = "#9ccfd8"; - color5 = "#c4a7e7"; - color13 = "#c4a7e7"; - color6 = "#ebbcba"; - color14 = "#ebbcba"; - color7 = "#e0def4"; - color15 = "#e0def4"; - - # tmux-like layout: panes via the splits layout, stack = zoom, tabs on a - # bottom status line with their index like tmux's window list. - enabled_layouts = "splits,stack"; - window_border_width = "1pt"; - inactive_text_alpha = "0.8"; - tab_bar_edge = "bottom"; - tab_bar_style = "powerline"; - tab_powerline_style = "slanted"; - tab_title_template = "{index}:{title}"; - active_tab_title_template = "{index}:{title}"; - scrollback_lines = 20000; - scrollback_pager = scrollbackPager; - shell_integration = "enabled"; - # Always zsh, whatever $SHELL the session was started with (a session - # begun before the login shell changed still carries SHELL=bash). - shell = "${pkgs.zsh}/bin/zsh"; - }; - - keybindings = { - # tabs = tmux windows - "${prefix}>c" = "new_tab_with_cwd"; - "${prefix}>n" = "next_tab"; - "${prefix}>p" = "previous_tab"; - "${prefix}>," = "set_tab_title"; - "${prefix}>&" = "close_tab"; - "${prefix}>w" = "select_tab"; - # panes = kitty windows - "${prefix}>-" = "launch --location=hsplit --cwd=current"; - "${prefix}>|" = "launch --location=vsplit --cwd=current"; - "${prefix}>x" = "close_window"; - "${prefix}>o" = "next_window"; - "${prefix}>z" = "toggle_layout stack"; - "${prefix}>space" = "next_layout"; - "${prefix}>h" = "neighboring_window left"; - "${prefix}>j" = "neighboring_window down"; - "${prefix}>k" = "neighboring_window up"; - "${prefix}>l" = "neighboring_window right"; - "${prefix}>shift+h" = "move_window left"; - "${prefix}>shift+j" = "move_window down"; - "${prefix}>shift+k" = "move_window up"; - "${prefix}>shift+l" = "move_window right"; - "${prefix}>{" = "move_window_backward"; - "${prefix}>}" = "move_window_forward"; - "${prefix}>shift+left" = "resize_window narrower 3"; - "${prefix}>shift+right" = "resize_window wider 3"; - "${prefix}>shift+up" = "resize_window taller 3"; - "${prefix}>shift+down" = "resize_window shorter 3"; - "${prefix}>=" = "resize_window reset"; - # copy mode and paste - "${prefix}>[" = "show_scrollback"; - "${prefix}>]" = "paste_from_clipboard"; - "${prefix}>u" = "open_url_with_hints"; - "${prefix}>f" = "kitten hints --type path --program -"; - # misc - "${prefix}>r" = "load_config_file"; - "${prefix}>?" = "kitten show_key -m kitty"; - "${prefix}>${prefix}" = "send_text all \\x01"; - } // tabKeys; - }; -} diff --git a/home/modules/tools.nix b/home/modules/tools.nix @@ -1,116 +0,0 @@ -# home/modules/tools.nix — runtime closure for the hand-written toolbox in -# ~/.local/bin, plus the general command-line tools. -# -# ~/.local/bin itself is not managed here on purpose: it is a flat git repo -# (vault README: "git init there afterwards so editing a file edits the live -# command"). NixOS puts it first on PATH (hosts/laptop/toolbox.nix). -{ config, pkgs, lib, ... }: -let - # ns [query]: fuzzy search of nixpkgs (+ NixOS and home-manager options) in - # the terminal, with the package description as the preview. nix-search-tv - # indexes search.nixos.org data locally on first run and refreshes it itself. - # Enter print the attribute name (e.g. to paste into tools.nix) - # ctrl-o open the homepage ctrl-s open the nixpkgs source - # ctrl-y copy the attribute name - ns = pkgs.writeShellScriptBin "ns" '' - nst=${pkgs.nix-search-tv}/bin/nix-search-tv - exec $nst print | ${pkgs.fzf}/bin/fzf \ - --query="$*" --scheme=history --prompt='nix › ' \ - --preview="$nst preview {}" --preview-window='right,60%,border-left,wrap' \ - --bind="ctrl-o:execute-silent(${pkgs.xdg-utils}/bin/xdg-open \$($nst homepage {}))" \ - --bind="ctrl-s:execute-silent(${pkgs.xdg-utils}/bin/xdg-open \$($nst source {}))" \ - --bind="ctrl-y:execute-silent(printf %s {} | ${pkgs.wl-clipboard}/bin/wl-copy)" \ - --header='enter: print · ctrl-y: copy · ctrl-o: homepage · ctrl-s: source' - ''; - - pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - cryptography - argon2-cffi - rich - questionary - pikepdf - mutagen - pillow - numpy - pyqt6 - youtube-transcript-api - ]); -in -{ - home.packages = with pkgs; [ - pythonEnv - ns - nix-search-tv # `ns`, and `nix-search-tv print|preview` by hand - perl - git - jujutsu - gnutar - zstd - pigz - xz - rsync - rclone - aria2 - p7zip - libarchive - gnupg - openssl - pinentry-gnome3 - ffmpeg - yt-dlp - atomicparsley - gallery-dl - imagemagick - img2pdf - resvg - zathura - calibre - poppler-utils - starship - bat - eza - fd - ripgrep - fzf - zoxide - atuin - jq - curl - wget - gh - fastfetch - wl-clipboard - libnotify - - # General tools (2026-10-08): what the dotfiles' zsh modules look for - # (modern.zsh, core.zsh) and what the stock Omarchy keys expect. - uv # PEP-723 scripts in ~/.local/bin (dcx, dorkforge); managed Pythons work via nix-ld - nodejs - btop - lazygit - lazydocker - tealdeer # `tldr` - dust - duf - procs - difftastic - hyperfine - glow - onefetch - tokei - xh - ncdu - parallel - unzip - zip - tree - file - cbonsai - cmatrix - localsend # phone ↔ laptop file drops on the LAN; port 53317 is open in hosts/laptop/default.nix - vesktop # Discord client; its config is linked from the dotfiles (dotfiles.nix) - ]; - - # Browser - programs.floorp.enable = true; -} diff --git a/home/modules/yazi.nix b/home/modules/yazi.nix @@ -1,130 +0,0 @@ -# home/modules/yazi.nix — yazi, the terminal file manager, with previews. -# -# Pictures preview inline in kitty (its graphics protocol; yazi draws them -# itself, nothing else needed), video frames via ffmpeg, PDFs via poppler, -# SVG via resvg, archives via 7z, JSON via jq, code with syntax colours, and -# the rest as text. Enter / l on a file opens it: images in imv, video and -# audio in mpv, PDFs and books in zathura, text in $EDITOR, anything else via -# xdg-open; o shows every opener. The toolbox's zimg/zbook/comx/gamex/dux -# are offered where they apply (they live in ~/.local/bin). -# -# y start yazi and cd to where you left it (zsh wrapper) -# T maximise the preview pane (toggle-pane) ! shell here -# <C-e>/<C-y> scroll the preview . toggle hidden -# l / Enter smart-enter: open a file, enter a directory -# Theme: Rosé Pine (home/yazi/flavors). Git status marks via the git plugin. -{ pkgs, ... }: -{ - programs.yazi = { - enable = true; - enableZshIntegration = true; - shellWrapperName = "y"; - extraPackages = with pkgs; [ - ffmpeg - poppler-utils - imagemagick - resvg - p7zip - jq - fd - ripgrep - fzf - zoxide - file - mediainfo - imv - ]; - - settings = { - mgr = { - show_hidden = false; - show_symlink = true; - sort_by = "natural"; - sort_sensitive = false; - sort_dir_first = true; - linemode = "size"; - scrolloff = 5; - }; - preview = { - max_width = 1600; - max_height = 1600; - image_delay = 20; - image_filter = "lanczos3"; - image_quality = 90; - wrap = "yes"; - }; - opener = { - edit = [ { run = ''${EDITOR:-nvim} "$@"''; desc = "Edit"; block = true; for = "unix"; } ]; - open = [ { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } ]; - view-image = [ - { run = ''imv "$@"''; desc = "View (imv)"; orphan = true; for = "unix"; } - { run = ''zimg "$1"''; desc = "View in zathura (zimg)"; orphan = true; for = "unix"; } - { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } - ]; - play = [ - { run = ''mpv "$@"''; desc = "Play (mpv)"; orphan = true; for = "unix"; } - { run = ''mpv --no-video "$@"''; desc = "Play audio only (mpv)"; block = true; for = "unix"; } - ]; - open-pdf = [ - { run = ''zathura "$@"''; desc = "Open (zathura)"; orphan = true; for = "unix"; } - { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } - ]; - open-book = [ - { run = ''zbook "$1"''; desc = "Read in zathura (zbook)"; orphan = true; for = "unix"; } - { run = ''zathura "$@"''; desc = "Open (zathura)"; orphan = true; for = "unix"; } - ]; - open-comic = [ - { run = ''comx "$1"''; desc = "Guided view (comx)"; orphan = true; for = "unix"; } - { run = ''zbook "$1"''; desc = "Panels in zathura (zbook)"; orphan = true; for = "unix"; } - { run = ''7z x -y "$1"''; desc = "Extract here (7z)"; block = true; for = "unix"; } - ]; - extract = [ { run = ''7z x -y "$1"''; desc = "Extract here (7z)"; for = "unix"; } ]; - run-exe = [ - { run = ''gamex run "$1"''; desc = "Run (gamex, dGPU)"; block = true; for = "unix"; } - { run = ''gamex proton "$1"''; desc = "Run with Proton-GE"; block = true; for = "unix"; } - ]; - }; - open = { - rules = [ - { mime = "image/gif"; use = [ "view-image" "open" ]; } - { mime = "image/*"; use = [ "view-image" "open" ]; } - { mime = "video/*"; use = [ "play" "open" ]; } - { mime = "audio/*"; use = [ "play" "open" ]; } - { mime = "application/pdf"; use = [ "open-pdf" "open" ]; } - { mime = "application/epub+zip"; use = [ "open-book" "open" ]; } - { name = "*.cbz"; use = [ "open-comic" "extract" ]; } - { name = "*.cbr"; use = [ "open-comic" "extract" ]; } - { name = "*.exe"; use = [ "run-exe" "open" ]; } - { mime = "application/{zip,gzip,x-tar,x-bzip*,x-7z-compressed,x-rar,x-xz,zstd}"; use = [ "extract" "open" ]; } - { mime = "inode/directory"; use = [ "edit" "open" ]; } - { mime = "text/*"; use = [ "edit" "open" ]; } - { mime = "application/{json,toml,x-ndjson,javascript,x-sh,x-shellscript,xml}"; use = [ "edit" "open" ]; } - { mime = "*"; use = [ "open" "edit" ]; } - ]; - }; - }; - - keymap.mgr.prepend_keymap = [ - { on = [ "!" ]; run = ''shell "$SHELL" --block''; desc = "Open a shell here"; } - { on = [ "<C-e>" ]; run = "seek 5"; desc = "Scroll preview down"; } - { on = [ "<C-y>" ]; run = "seek -5"; desc = "Scroll preview up"; } - { on = [ "l" ]; run = "plugin smart-enter"; desc = "Enter directory or open file"; } - { on = [ "<Enter>" ]; run = "plugin smart-enter"; desc = "Enter directory or open file"; } - { on = [ "T" ]; run = "plugin toggle-pane max-preview"; desc = "Maximise the preview"; } - { on = [ "u" "d" ]; run = "shell -- dux %h %s"; desc = "Copy file(s) to the clipboard as a paste-able file"; } - ]; - - plugins = with pkgs.yaziPlugins; { - inherit full-border git smart-enter toggle-pane; - }; - flavors.rose-pine = ../yazi/flavors/rose-pine.yazi; - theme.flavor = { - dark = "rose-pine"; - light = "rose-pine"; - }; - initLua = '' - require("full-border"):setup() - require("git"):setup() - ''; - }; -} diff --git a/hosts/bootstrap/default.nix b/hosts/bootstrap/default.nix @@ -1,89 +0,0 @@ -# hosts/bootstrap/default.nix -# -# Stage A. The GNOME install exactly as the installer left it (this is the old -# configuration.nix, reorganised) plus: -# - the dead-GPU-fan workaround and its sensor driver, -# - the toolbox prerequisites (envfs, ~/.local/bin on PATH, padx udev rule, -# a python3 with the tools' modules, jq, fzf), -# - the Hyprland binary cache, so the `nixos` configuration downloads -# instead of compiling. -# -# Built from nixpkgs-stable (the revision this machine runs), so the switch is -# small and GNOME stays untouched. Apply, reboot (new kernel modules only load -# from the booted system), verify with `fanfix status`, then move on to -# `nixos`. Delete this directory and the nixpkgs-stable input afterwards. -{ config, pkgs, lib, user, ... }: -{ - imports = [ - ../laptop/hardware-configuration.nix - ../laptop/fan-throttle-guard.nix - ../laptop/fan-extras.nix - ../laptop/uniwill-laptop.nix - ../laptop/nix-settings.nix - ../laptop/toolbox.nix - ]; - - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - - networking.hostName = "nixos"; - networking.networkmanager.enable = true; - - time.timeZone = "Europe/Isle_of_Man"; - i18n.defaultLocale = "en_US.UTF-8"; - - services.displayManager.gdm.enable = true; - services.desktopManager.gnome.enable = true; - services.xserver.xkb = { - layout = "us"; - variant = ""; - }; - - services.printing.enable = true; - - services.pulseaudio.enable = false; - security.rtkit.enable = true; - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - }; - - users.users.${user} = { - isNormalUser = true; - description = "daemon-sec"; - extraGroups = [ "networkmanager" "wheel" ]; - }; - - programs.firefox.enable = true; - nixpkgs.config.allowUnfree = true; - - # Enough of the toolbox runtime to use ~/.local/bin from GNOME meanwhile. - # The full closure arrives with home-manager in the `nixos` configuration. - environment.systemPackages = with pkgs; [ - (python3.withPackages (ps: with ps; [ - cryptography - argon2-cffi - rich - questionary - pikepdf - mutagen - pillow - numpy - pyqt6 - youtube-transcript-api - ])) - perl - jq - fzf - bat - eza - fd - ripgrep - git - jujutsu # jj, colocated with the existing .git; also in home/modules/tools.nix for Stage B - ]; - - system.stateVersion = "26.05"; -} diff --git a/hosts/laptop/default.nix b/hosts/laptop/default.nix @@ -1,142 +0,0 @@ -# hosts/laptop/default.nix -# -# PCSpecialist Valeon II 17 (TongFang GM7RGxM): Ryzen 9 6900HX, Radeon 680M at -# 06:00.0, RTX 3070 Ti Laptop at 01:00.0, 2560x1440@240 panel. Hyprland under -# uwsm, Caelestia Shell from home-manager (home/), greetd + tuigreet to log in. -{ config, pkgs, lib, user, ... }: -{ - imports = [ - ./hardware-configuration.nix - ./fan-throttle-guard.nix # dead-GPU-fan workaround (vault gpu-fan-fix/, imported unchanged) - ./fan-extras.nix # the one imperative step fanfix install did: the performance profile - ./uniwill-laptop.nix # the `uniwill` hwmon the guard reads: kernel 6.19 driver built for this kernel - ./nvidia.nix - ./ssd.nix - ./nix-settings.nix - ./toolbox.nix - ./sops.nix # sops-nix: secrets/secrets.yaml → /run/secrets - ./fan-cli.nix # fan-ec: root side of the `fan` command (home/modules/fan.nix), passwordless for wheel - ]; - - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - - networking.hostName = "nixos"; - networking.networkmanager.enable = true; - # LocalSend (home/modules/tools.nix) discovers peers and receives on 53317. - networking.firewall.allowedTCPPorts = [ 53317 ]; - networking.firewall.allowedUDPPorts = [ 53317 ]; - - time.timeZone = "Europe/Isle_of_Man"; - i18n.defaultLocale = "en_US.UTF-8"; - services.xserver.xkb = { - layout = "us"; - options = "compose:caps,shift:both_capslock_cancel"; - }; - - nixpkgs.config.allowUnfree = true; # nvidia, obsidian, claude-desktop - - users.users.${user} = { - isNormalUser = true; - description = "daemon-sec"; - extraGroups = [ "networkmanager" "wheel" ]; - shell = pkgs.zsh; - }; - - ##### Shell ################################################################## - # zsh is the login shell. Its configuration is the dotfiles' ZDOTDIR tree - # (home/modules/shell.nix): core.zsh runs a cached compinit and theme.zsh - # starts starship, so the global compinit and the default prompt stay off. - # /etc/zshrc still puts every profile's share/zsh/site-functions on fpath. - programs.zsh = { - enable = true; - enableGlobalCompInit = false; - promptInit = ""; - }; - - # Binaries that are not built by Nix (uv-managed Pythons and their wheels, - # anything mise or npm downloads) expect /lib64/ld-linux-x86-64.so.2. - programs.nix-ld.enable = true; - - ##### Desktop ################################################################ - # Hyprland package and portal come from inputs.hyprland.nixosModules.default. - programs.hyprland = { - enable = true; - withUWSM = true; # systemd-managed session; graphical-session.target starts Caelestia, polkit agent, udiskie - xwayland.enable = true; - }; - - # Display manager: greetd with the tuigreet text greeter. Remembers the last - # user and session, so a boot is: password, Enter. No theme engine, no X. - services.greetd = { - enable = true; - useTextGreeter = true; - settings.default_session.command = lib.concatStringsSep " " [ - "${pkgs.tuigreet}/bin/tuigreet" - "--time" - "--remember" - "--remember-session" - "--asterisks" - "--sessions ${config.services.displayManager.sessionData.desktops}/share/wayland-sessions" - ]; - }; - security.pam.services.greetd.enableGnomeKeyring = true; # unlock the keyring at login (Claude desktop uses it) - - xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file chooser; hyprland portal does screencast - security.polkit.enable = true; # agent: hyprpolkitagent user service (home/modules/hyprland.nix) - services.udisks2.enable = true; # udiskie - services.power-profiles-daemon.enable = true; # the fan fix depends on it - services.gnome.gnome-keyring.enable = true; - programs.dconf.enable = true; - - services.pulseaudio.enable = false; - security.rtkit.enable = true; - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - wireplumber.enable = true; - }; - - programs.firefox.enable = true; - services.printing.enable = true; - - programs.gnupg.agent = { - enable = true; - pinentryPackage = pkgs.pinentry-gnome3; - }; - - ##### Fonts ################################################################## - # DMMono Nerd Font is not in nixpkgs; home/modules/tools.nix links it from - # ~/git/daemon-sec-dotfiles into ~/.local/share/fonts. - fonts.packages = with pkgs; [ - noto-fonts - noto-fonts-color-emoji - noto-fonts-cjk-sans - rubik # Caelestia clock font - material-symbols # Caelestia icons - ]; - fonts.fontconfig.defaultFonts = { - monospace = [ "DMMono Nerd Font" "Noto Sans Mono" ]; - sansSerif = [ "Noto Sans" ]; - serif = [ "Noto Serif" ]; - emoji = [ "Noto Color Emoji" ]; - }; - - ##### Session environment #################################################### - # uwsm imports these through the login shell. GPU-specific ones are in nvidia.nix. - environment.sessionVariables = { - ELECTRON_OZONE_PLATFORM_HINT = "auto"; - GDK_BACKEND = "wayland,x11"; - QT_QPA_PLATFORM = "wayland;xcb"; - QT_WAYLAND_DISABLE_WINDOWDECORATION = "1"; - }; - - environment.systemPackages = with pkgs; [ - pciutils # lspci - usbutils - ]; - - system.stateVersion = "26.05"; -} diff --git a/hosts/laptop/fan-cli.nix b/hosts/laptop/fan-cli.nix @@ -1,122 +0,0 @@ -# hosts/laptop/fan-cli.nix — root side of the `fan` command (home/modules/fan.nix). -# -# `fan-ec` talks to the embedded controller the way fanfix does (same -# acpi_call recipe, same registers; see ~/.local/bin/fanfix and the TUXEDO -# driver), but it is a fixed script in the Nix store, so the wheel group may -# run it through sudo without a password. That is what lets the watchdog in -# fan.nix put the fans back to EC-automatic from a background unit, where -# sudo could not ask for one. fanfix itself lives in the user-writable -# ~/.local/bin and must never get such a rule. -# -# fan-ec status mode, duty %, EC flags fan-ec max 100 % (manual) -# fan-ec auto hand control back to the EC fan-ec <30-100> fixed % (manual) -# fan-ec mode one word: auto | manual | curve-daemon -# -# Manual mode on this laptop makes the EC assert PROCHOT (all cores 399 MHz) -# under load (fan-throttle-guard.nix, line ~97). fan-ec does not try to -# prevent that; the user-side watchdog detects the clamp and calls `fan-ec auto`. -{ pkgs, lib, ... }: -let - fan-ec = pkgs.writeShellScriptBin "fan-ec" '' - set -uo pipefail - [ "$(id -u)" = 0 ] || { echo "fan-ec: run as root (sudo fan-ec …)" >&2; exit 1; } - PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.kmod pkgs.systemd ]}:$PATH - - ACPI_CALL=/proc/acpi/call - EC_DEV='\_SB.INOU' - FAN_UNIT=fanfix-fan.service - FAN_MIN_PCT=30 - - ec_ready() { [ -w "$ACPI_CALL" ] || modprobe acpi_call 2>/dev/null; [ -w "$ACPI_CALL" ]; } - ec_raw() { printf '%s' "$1" > "$ACPI_CALL" || return 1; local o; o=$(tr -d '\0\n\r ' < "$ACPI_CALL"); printf '%s' "$o"; } - ec_read() { local out; out=$(ec_raw "$(printf '%s.ECRR 0x%04x' "$EC_DEV" "$1")") || return 1 - [[ "$out" =~ ^0x[0-9a-fA-F]+$ ]] || { echo "EC read $(printf '0x%04x' "$1") failed: $out" >&2; return 1; } - echo $(( out )); } - ec_write() { local out; out=$(ec_raw "$(printf '%s.ECRW 0x%04x 0x%02x' "$EC_DEV" "$1" "$2")") || return 1 - case "$out" in Error*|"not called"|"") echo "EC write $(printf '0x%04x' "$1") failed: ''${out:-no result}" >&2; return 1;; esac - sleep 0.005; } - ec_set_bits() { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v | $2 )); } - ec_clear_bits() { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v & ~$2 & 0xff )); } - ec_bit() { local v; v=$(ec_read "$1") || return 1; echo $(( (v >> $2) & 1 )); } - - R_AP_OEM=0x0741; R_FAN_MODE=0x0751; R_PWM1=0x075B; R_PWM2=0x075C - R_FAN_CTRL=0x078E; R_TBL_SPLIT=0x07C5; R_TBL_ENABLE=0x07C6 - R_CPU_TBL_END=0x0F00; R_CPU_TBL_START=0x0F10; R_CPU_TBL_SPEED=0x0F20 - R_GPU_TBL_END=0x0F30; R_GPU_TBL_START=0x0F40; R_GPU_TBL_SPEED=0x0F50 - R_PWM1_W=0x1804; R_PWM2_W=0x1809 - - universal_ctrl() { ec_bit $R_FAN_CTRL 6; } - tables_enabled() { ec_bit $R_TBL_ENABLE 2; } - pct_to_duty() { echo $(( $1 * 200 / 100 )); } - duty_to_pct() { echo $(( $1 * 100 / 200 )); } - - fan_init_tables() { - local i - ec_clear_bits $R_FAN_MODE 0x40 - [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] || ec_set_bits $R_TBL_SPLIT 0x80 - ec_write $R_CPU_TBL_END 115; ec_write $R_CPU_TBL_START 0; ec_write $R_CPU_TBL_SPEED 1 - ec_write $R_GPU_TBL_END 120; ec_write $R_GPU_TBL_START 0; ec_write $R_GPU_TBL_SPEED 1 - for i in $(seq 1 15); do - ec_write $(( R_CPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_CPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_CPU_TBL_SPEED + i )) 200 - ec_write $(( R_GPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_GPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_GPU_TBL_SPEED + i )) 200 - done - [ "$(tables_enabled)" = 1 ] || ec_set_bits $R_TBL_ENABLE 0x04 - } - fan_apply_duty() { - local d=$1 - if [ "$(universal_ctrl)" = 1 ]; then - [ "$(tables_enabled)" = 1 ] && [ "$(ec_bit $R_FAN_MODE 6)" = 0 ] || fan_init_tables - ec_write $R_CPU_TBL_SPEED "$d"; ec_write $R_GPU_TBL_SPEED "$d" - ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d" - else - local i; [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || ec_set_bits $R_FAN_MODE 0x40 - for i in 1 2 3 4 5 6 7 8 9 10; do ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d"; sleep 0.01; done - fi - } - fan_set_auto() { - if [ "$(universal_ctrl)" = 1 ]; then - [ "$(tables_enabled)" = 1 ] && ec_clear_bits $R_TBL_ENABLE 0x04 - [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] && ec_clear_bits $R_TBL_SPLIT 0x80 - fi - [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] && ec_clear_bits $R_FAN_MODE 0x40 - return 0 - } - mode_word() { - if systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null; then echo curve-daemon; return; fi - if [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || [ "$(tables_enabled)" = 1 ]; then echo manual; else echo auto; fi - } - stop_daemon() { systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null && systemctl stop "$FAN_UNIT"; return 0; } - guard() { ec_ready || { echo "fan-ec: acpi_call not available" >&2; exit 1; } - ec_read $R_AP_OEM >/dev/null || { echo "fan-ec: EC not reachable via $EC_DEV.ECRR" >&2; exit 1; }; } - - case "''${1:-status}" in - mode) guard; mode_word ;; - status) guard - printf 'mode=%s duty=%s%% target=%s%% fan-abnormal=%s full-fan-bit=%s tables=%s\n' \ - "$(mode_word)" "$(duty_to_pct "$(ec_read $R_PWM1)")" \ - "$([ "$(tables_enabled)" = 1 ] && duty_to_pct "$(ec_read $R_CPU_TBL_SPEED)" || echo -)" \ - "$(ec_bit $R_AP_OEM 5)" "$(ec_bit $R_FAN_MODE 6)" "$(tables_enabled)" ;; - auto) guard; stop_daemon; fan_set_auto && echo "fans: auto (EC curve)" ;; - max) guard; stop_daemon; fan_apply_duty 200 && echo "fans: 100 % (manual)" ;; - [0-9]*) p=''${1%\%}; [[ "$p" =~ ^[0-9]+$ ]] && [ "$p" -le 100 ] && [ "$p" -ge "$FAN_MIN_PCT" ] \ - || { echo "fan-ec: percent must be $FAN_MIN_PCT-100 (below that the EC trips at 79 °C)" >&2; exit 2; } - guard; stop_daemon; fan_apply_duty "$(pct_to_duty "$p")" && echo "fans: $p % (manual)" ;; - *) echo "usage: fan-ec status|mode|auto|max|<30-100>" >&2; exit 2 ;; - esac - ''; -in -{ - environment.systemPackages = [ fan-ec ]; - - # wheel may run fan-ec without a password: it is immutable store content - # (via the system profile symlink, which is root-owned), does one thing, - # and the watchdog has no terminal to type into. - security.sudo.extraRules = [ - { - groups = [ "wheel" ]; - commands = [ - { command = "/run/current-system/sw/bin/fan-ec"; options = [ "NOPASSWD" ]; } - ]; - } - ]; -} diff --git a/hosts/laptop/fan-extras.nix b/hosts/laptop/fan-extras.nix @@ -1,25 +0,0 @@ -# hosts/laptop/fan-extras.nix -# -# The part of the fan fix that `fanfix install` did imperatively on Arch and -# that fan-throttle-guard.nix (imported unchanged) does not carry: the -# "performance" power profile. power-profiles-daemon persists the choice, so -# this oneshot is idempotent; it runs after ppd is up and then re-applies the -# tmpfiles clock floor, because a profile switch rewrites per-policy boost and -# can lift scaling_max_freq (fanfix re-runs tmpfiles for the same reason; the -# stability guard would also catch it within a second). -{ pkgs, lib, ... }: -{ - systemd.services.fanfix-performance-profile = { - description = "fanfix: select the performance power profile and re-assert the clock floor"; - after = [ "power-profiles-daemon.service" "systemd-tmpfiles-setup.service" ]; - requires = [ "power-profiles-daemon.service" ]; - # graphical.target, not multi-user: on NixOS power-profiles-daemon is itself - # ordered After=multi-user.target, so multi-user here is an ordering cycle. - wantedBy = [ "graphical.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${pkgs.power-profiles-daemon}/bin/powerprofilesctl set performance"; - ExecStartPost = "${pkgs.systemd}/bin/systemd-tmpfiles --create --prefix=/sys/devices/system/cpu"; - }; - }; -} diff --git a/hosts/laptop/fan-throttle-guard.nix b/hosts/laptop/fan-throttle-guard.nix @@ -1,122 +0,0 @@ -# hosts/laptop/fan-throttle-guard.nix -# -# Dead-GPU-fan workaround for the PCSpecialist Valeon II 17 (TongFang GM7RGxM, -# Ryzen 9 6900HX + RTX 3070 Ti Laptop). The embedded controller sees the dead -# "Secondary" fan (fan2: 0 rpm while commanded 100 %), raises its fan-abnormal -# flag and, once Tctl reaches ~79 °C, asserts PROCHOT and pins all 16 threads at -# 399 MHz until ~47 °C. That policy is firmware; Linux cannot switch it off. -# The fix is to keep the CPU from ever reaching the trip point: -# -# 1. a static scaling_max_freq floor of 3.2 GHz applied by tmpfiles at boot -# (3.2 GHz ≈ base clock → ~67-70 °C under all-core load, no trips), -# 2. a staged guard (3200 → 2400 → 1800 MHz) driven by k10temp + the uniwill -# board sensor, which also covers the "latched" low-temperature clamp, -# 3. the surviving CPU fan held at 60 % duty through the EC's own ACPI -# methods (acpi_call → \_SB.INOU.ECRR/ECRW, TUXEDO register recipe), -# 4. power-profiles-daemon kept on, profile "performance", and the global -# cpufreq boost flag left at 1 — never use boost=0, ppd 0.30 writes -# per-policy boost on every switch and fails with EINVAL otherwise. -# -# Everything here was measured on the Arch install this was captured from -# (fanfix 2026-08-23, stability guard 2026-09-07). Files beside this module: -# fanfix the CLI/daemon (bash) ← copied verbatim -# stability_guard.py the staged ceiling (python3) ← copied verbatim -# fan-ctl, fan-state bar-widget helpers; need a polkit agent and a bar slot -# -# Verify on first boot: fanfix status · fanfix fan status · fanfix test 30 -# expected: cap 3200 MHz, boost 1, profile performance, no THROTTLE event, -# peak < 75 °C. If a trip still happens: lower the floor to 3000000 below. -{ config, pkgs, lib, ... }: - -let - # fanfix is plain bash; wrap it so the shebang resolves and PATH is supplied - # by the unit (fanDeps) rather than by whatever shell invoked it. - fanfix = pkgs.writeShellScriptBin "fanfix" (builtins.readFile ./fanfix); - - fanDeps = with pkgs; [ - coreutils gnugrep gawk gnused procps util-linux - kmod # modprobe acpi_call / uniwill-laptop - systemd # systemctl, systemd-tmpfiles - power-profiles-daemon # powerprofilesctl - ]; - - capKhz = 3200000; # the floor. 3000000 is the documented fallback. -in -{ - ##### 1. EC access and fan/temperature readout ############################## - # acpi_call is out-of-tree (nixpkgs: linuxPackages.acpi_call). uniwill-laptop - # is in-tree; `force=1` is required because the DMI match list does not carry - # this GM7RGxM. Verify `modinfo uniwill-laptop` exists on the chosen kernel. - boot.extraModulePackages = [ config.boot.kernelPackages.acpi_call ]; - boot.kernelModules = [ "acpi_call" "uniwill-laptop" ]; - boot.extraModprobeConfig = '' - options uniwill-laptop force=1 - ''; - - ##### 2. Static floor, applied before any user load exists ################### - systemd.tmpfiles.rules = [ - "w /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq - - - - ${toString capKhz}" - ]; - - ##### 3. power-profiles-daemon stays on ###################################### - services.power-profiles-daemon.enable = true; - - ##### 4. Staged thermal ceiling (replaces /etc/systemd/system/motherboard-stability.service) - systemd.services.motherboard-stability = { - description = "CPU stability limits for the GM7RGxM fan/power fault workaround"; - after = [ "systemd-tmpfiles-setup.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "simple"; - ExecStart = "${pkgs.python3}/bin/python3 -I ${./stability_guard.py}"; - Restart = "on-failure"; - RestartSec = 3; - RuntimeDirectory = "motherboard-stability"; - RuntimeDirectoryMode = "0755"; - NoNewPrivileges = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [ "/sys/devices/system/cpu" "/run/motherboard-stability" ]; - PrivateTmp = true; - PrivateDevices = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictAddressFamilies = "AF_UNIX"; - LockPersonality = true; - RestrictSUIDSGID = true; - CapabilityBoundingSet = ""; - UMask = "0022"; - }; - }; - - ##### 5. Surviving CPU fan at a fixed 60 % duty ############################## - # CURVE is "temp:pct …" pairs; a flat 0:60 100:60 is what has been running. - # fanfix refuses anything below 30 %. Edit here, not in /etc, then rebuild. - # - # NOT started at boot (wantedBy = []). Measured 2026-10-07 on NixOS: while the - # daemon holds the EC in manual/custom-table fan mode, the EC asserts PROCHOT - # (all cores 399 MHz) the moment any load starts, even at 37 °C. Stopping the - # unit and `fanfix fan auto` cleared it instantly; 10 s all-core test then ran - # at 3112 MHz, peak 53 °C, 0 trips. The 3.2 GHz floor + stability guard are - # enough on their own. Start by hand to experiment: systemctl start fanfix-fan - environment.etc."fanfix.conf".text = '' - CURVE="0:60 100:60" - ''; - - systemd.services.fanfix-fan = { - description = "fanfix: temperature → fan-duty curve for the surviving CPU fan (dead GPU fan workaround)"; - after = [ "multi-user.target" ]; - wantedBy = [ ]; # see note above; manual start only - path = fanDeps; - serviceConfig = { - Type = "simple"; - ExecStart = "${fanfix}/bin/fanfix fan-daemon"; - ExecStopPost = "${fanfix}/bin/fanfix fan-release"; - Restart = "on-failure"; - RestartSec = 5; - }; - }; - - ##### 6. Tools on PATH ###################################################### - environment.systemPackages = [ fanfix pkgs.lm_sensors ] ++ fanDeps; -} diff --git a/hosts/laptop/hardware-configuration.nix b/hosts/laptop/hardware-configuration.nix @@ -1,45 +0,0 @@ -# Do not modify this file! It was generated by ‘nixos-generate-config’ -# and may be overwritten by future invocations. Please make changes -# to /etc/nixos/configuration.nix instead. -{ config, lib, pkgs, modulesPath, ... }: - -{ - imports = - [ (modulesPath + "/installer/scan/not-detected.nix") - ]; - - boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ]; - boot.initrd.kernelModules = [ ]; - boot.kernelModules = [ "kvm-amd" ]; - boot.extraModulePackages = [ ]; - - fileSystems."/" = - { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; - fsType = "btrfs"; - }; - - fileSystems."/home" = - { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; - fsType = "btrfs"; - options = [ "subvol=home" ]; - }; - - fileSystems."/nix" = - { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; - fsType = "btrfs"; - options = [ "subvol=nix" ]; - }; - - fileSystems."/boot" = - { device = "/dev/disk/by-uuid/D3FC-22C2"; - fsType = "vfat"; - options = [ "fmask=0077" "dmask=0077" ]; - }; - - swapDevices = - [ { device = "/dev/disk/by-uuid/6aee8a42-a2a1-4d18-8f5c-695195e0c122"; } - ]; - - nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; - hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; -} diff --git a/hosts/laptop/nix-settings.nix b/hosts/laptop/nix-settings.nix @@ -1,41 +0,0 @@ -# hosts/laptop/nix-settings.nix — nix daemon settings and the nh helper. -# Imported by both the `nixos` target and the `bootstrap` stage. -{ pkgs, ... }: -{ - nix.settings = { - experimental-features = [ "nix-command" "flakes" ]; - # Hyprland is built from its own flake pins, which cache.nixos.org does - # not have. Without the Hyprland cache every update compiles it locally. - substituters = [ - "https://cache.nixos.org" - "https://hyprland.cachix.org" - ]; - trusted-public-keys = [ - "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" - "hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc=" - ]; - }; - - # nh, the Nix helper: `nh os switch|boot|test`, `nh clean all`, `nh search`. - # Wraps nixos-rebuild with nix-output-monitor progress and an nvd diff of - # what a generation changes, and asks for sudo only for the switch itself. - # NH_FLAKE points at this repo, so `nh os boot` works from any directory; - # the configuration is picked by hostname (`nixos`), or with -H <name>. - programs.nh = { - enable = true; - flake = "/home/daemonsec/NixDaemon"; - clean = { - enable = true; # weekly `nh clean all`: drops old generations and runs the GC, - dates = "weekly"; # keeping the last 5 and anything newer than 14 days - extraArgs = "--keep 5 --keep-since 14d"; - }; - }; - - # The two tools nh builds on, also useful by hand: - # nvd diff /run/current-system result what a build would change - # nom build .#… nix build with a live tree view - environment.systemPackages = with pkgs; [ - nvd - nix-output-monitor - ]; -} diff --git a/hosts/laptop/nvidia.nix b/hosts/laptop/nvidia.nix @@ -1,53 +0,0 @@ -# hosts/laptop/nvidia.nix -# -# The panel (eDP-1) is wired to the RTX 3070 Ti at 01:00.0: the firmware MUX is -# in discrete mode (amdgpu's eDP-2 reports disconnected). Linux cannot change -# the MUX, so this configures what the hardware presents: NVIDIA open kernel -# module with modesetting, the Radeon 680M left as a secondary DRM device. -# -# PARKED: if the MUX is switched to hybrid in the BIOS, swap the AQ_DRM_DEVICES -# order (igpu-card first) and add the prime offload block at the bottom. -{ config, pkgs, lib, ... }: -{ - services.xserver.videoDrivers = [ "nvidia" ]; - - hardware.graphics = { - enable = true; - extraPackages = [ pkgs.nvidia-vaapi-driver ]; - }; - - hardware.nvidia = { - open = true; # GA104 is supported by the open kernel modules - modesetting.enable = true; # nvidia-drm.modeset=1 - package = config.boot.kernelPackages.nvidiaPackages.stable; - nvidiaSettings = false; - # powerManagement.enable = true; # suspend/resume helpers; untested on this laptop, left at default - }; - - # Stable, colon-free names for the two DRM cards. Aquamarine splits - # AQ_DRM_DEVICES on ':', so the /dev/dri/by-path names cannot go in it (the PCI - # address has colons): it chopped them into "pci-0000", "01", "00.0-card", found - # no GPU and Hyprland aborted at startup with "CBackend::create() failed!". - # ID_PATH is matched exactly so the boot-time simpledrm card (whose ID_PATH is - # pci-0000:01:00.0-platform-simple-framebuffer.0) does not take the dGPU name. - services.udev.extraRules = '' - SUBSYSTEM=="drm", KERNEL=="card[0-9]*", ENV{ID_PATH}=="pci-0000:01:00.0", SYMLINK+="dri/dgpu-card" - SUBSYSTEM=="drm", KERNEL=="card[0-9]*", ENV{ID_PATH}=="pci-0000:06:00.0", SYMLINK+="dri/igpu-card" - ''; - - environment.sessionVariables = { - # Stable device order for Hyprland/aquamarine: dGPU (panel) first, iGPU second. - AQ_DRM_DEVICES = "/dev/dri/dgpu-card:/dev/dri/igpu-card"; - LIBVA_DRIVER_NAME = "nvidia"; - __GLX_VENDOR_LIBRARY_NAME = "nvidia"; - NVD_BACKEND = "direct"; - }; - - # Hybrid (iGPU drives the panel, dGPU on demand). Only if the BIOS MUX is set to hybrid: - # hardware.nvidia.prime = { - # offload.enable = true; - # offload.enableOffloadCmd = true; - # amdgpuBusId = "PCI:6:0:0"; - # nvidiaBusId = "PCI:1:0:0"; - # }; -} diff --git a/hosts/laptop/sops.nix b/hosts/laptop/sops.nix @@ -1,32 +0,0 @@ -# hosts/laptop/sops.nix — sops-nix: secrets encrypted in this repo, decrypted -# at activation into /run/secrets (root-only tmpfs; per-secret owner/mode). -# -# One age identity does everything (.sops.yaml): the user edits with the sops -# CLI (reads ~/.config/sops/age/keys.txt), the system decrypts at boot with a -# root-only copy at /var/lib/sops-nix/key.txt. Put it there once: -# -# sudo install -D -m 600 -o root -g root ~/.config/sops/age/keys.txt /var/lib/sops-nix/key.txt -# -# No SSH host key is used: sshd is not enabled on this machine, so there is -# none to derive an age key from (sshKeyPaths is emptied below for that reason). -# -# Declaring a secret: -# sops.secrets.my-token = { }; # → /run/secrets/my-token, root:root 0400 -# sops.secrets."ssd.key" = { path = "/etc/secrets/ssd.key"; }; # the LUKS key, once it is -# # added to secrets/secrets.yaml (sops set …) -# sops.secrets.wifi-psk = { owner = user; }; # readable by the user -# then `sops secrets/secrets.yaml` to add the value, and `nh os switch`. -{ inputs, user, ... }: -{ - imports = [ inputs.sops-nix.nixosModules.sops ]; - - sops = { - defaultSopsFile = ../../secrets/secrets.yaml; - age = { - keyFile = "/var/lib/sops-nix/key.txt"; - sshKeyPaths = [ ]; - generateKey = false; # the key is the user's (see header), never a fresh one - }; - gnupg.sshKeyPaths = [ ]; - }; -} diff --git a/hosts/laptop/ssd.nix b/hosts/laptop/ssd.nix @@ -1,30 +0,0 @@ -# hosts/laptop/ssd.nix -# -# Spare Samsung SSD 980 1 TB (LUKS2, btrfs label SSD) at /mnt/ssd, unlocked at -# boot from /etc/secrets/ssd.key. The key is restored by hand once (vault -# samsung-ssd.md, section 2): -# -# sudo mkdir -p -m 700 /etc/secrets -# gpg -d --pinentry-mode loopback ssd.key.gpg | sudo tee /etc/secrets/ssd.key >/dev/null -# sudo chmod 400 /etc/secrets/ssd.key -# -# `nofail` on both lines: the laptop boots normally while the key (or the -# drive) is missing; the unit just fails. Swap the key path for `none` to type -# the passphrase at boot instead. -{ ... }: -{ - environment.etc.crypttab.text = '' - ssd UUID=8cd17d0f-adf3-430f-9d95-d17ef42df9b0 /etc/secrets/ssd.key luks,nofail - ''; - - fileSystems."/mnt/ssd" = { - device = "/dev/mapper/ssd"; - fsType = "btrfs"; - options = [ - "compress=zstd:3" - "noatime" - "nofail" - "x-systemd.device-timeout=10s" - ]; - }; -} diff --git a/hosts/laptop/toolbox.nix b/hosts/laptop/toolbox.nix @@ -1,26 +0,0 @@ -# hosts/laptop/toolbox.nix -# -# NixOS-side support for the hand-written toolbox that lives, flat, in -# ~/.local/bin (its own git repo; not in the Nix store). Shared by the target -# and the bootstrap configuration. -{ pkgs, lib, user, ... }: -{ - # Several tools keep Arch-style shebangs (#!/bin/bash: fanfix, dropterm, - # omarchy-menu-*; #!/usr/bin/python3: lid-control). envfs resolves those - # paths from the caller's PATH instead of patching the scripts. - services.envfs.enable = true; - - # ~/.local/bin first on PATH (prepends in /etc/set-environment). - environment.localBinInPath = true; - - users.users.${user}.extraGroups = [ - "input" # padx talks to the touchpad over hidraw - "video" - ]; - - # padx: the Pixart 093A:0274 touchpad behind the UNIW0001 I2C-HID bridge. - # Replaces ~/trackpad-fix/60-padx-touchpad.rules from the Arch install. - services.udev.extraRules = '' - KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{modalias}=="hid:b0018g*v0000093Ap00000274", GROUP="input", MODE="0660" - ''; -} diff --git a/hosts/laptop/uniwill-laptop.nix b/hosts/laptop/uniwill-laptop.nix @@ -1,20 +0,0 @@ -# hosts/laptop/uniwill-laptop.nix -# -# fan-throttle-guard.nix expects the in-tree `uniwill-laptop` driver (the -# `uniwill` hwmon: fan1/fan2 rpm, pwm1/pwm2, board temps; also the keyboard -# backlight LED and the touchpad-toggle bit padx mentions). Upstream merged it -# in Linux 6.19; the nixpkgs 6.18 kernel predates it and the 7.2 kernel config -# leaves X86_PLATFORM_DRIVERS_UNIWILL off. Building the v6.19 sources as an -# out-of-tree module against whatever boot.kernelPackages selects is the cheap -# fix: a 10-second compile, no custom kernel. Verified to build on 6.18.55. -# -# Without this hwmon stability_guard.py pins the CPU at 1.8 GHz ("essential -# sensor or main fan unavailable"), so this file is not optional. -{ config, lib, pkgs, ... }: -{ - boot.extraModulePackages = [ - (config.boot.kernelPackages.callPackage ./uniwill-laptop/package.nix { }) - ]; - # `boot.kernelModules` and the `force=1` modprobe option (this GM7RGxM is not - # in the driver's DMI list) are set in fan-throttle-guard.nix. -} diff --git a/modules/features/desktop/hyprland.nix b/modules/features/desktop/hyprland.nix @@ -0,0 +1,24 @@ +# modules/features/desktop/hyprland.nix — the NixOS side of the Hyprland +# desktop, on when daemon.desktop.hyprland.enable (modules/features/desktop/options.nix). +# The compositor config, Caelestia and the session services are home-manager +# modules (modules/home/hyprland.nix, modules/home/caelestia.nix), gated the same way. +{ ... }: +{ + flake.nixosModules.desktop-hyprland = + { config, lib, pkgs, inputs, ... }: + { + # Hyprland package and portal come from inputs.hyprland.nixosModules.default. + imports = [ inputs.hyprland.nixosModules.default ]; + + config = lib.mkIf config.daemon.desktop.hyprland.enable { + programs.hyprland = { + enable = true; + withUWSM = true; # systemd-managed session; graphical-session.target starts Caelestia, polkit agent, udiskie + xwayland.enable = true; + }; + + xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file chooser; hyprland portal does screencast + }; + } + ; +} diff --git a/modules/features/desktop/niri.nix b/modules/features/desktop/niri.nix @@ -0,0 +1,182 @@ +# modules/features/desktop/niri.nix — Niri (scrolling-column Wayland +# compositor) with Noctalia Shell, as a wrapped, portable package plus the +# NixOS module that installs it as a login session. +# +# nix run ~/NixDaemon#niri try it nested inside the running desktop (Alt is the modifier) +# daemon.desktop.niri.enable the switch (modules/features/desktop/options.nix) +# +# The settings below become niri's config.kdl at build time (the wrapper runs +# `niri validate` on it, so a bad bind fails the build, not the login) and the +# package carries every program its binds spawn. Keys mirror the Hyprland set +# (modules/home/hypr/defaults.lua) where a Niri or Noctalia counterpart exists. +# Rosé Pine Main: the focus ring is rose #ebbcba on overlay #26233a. +{ self, inputs, ... }: +{ + perSystem = + { pkgs, lib, self', ... }: + let + noctalia = lib.getExe self'.packages.noctalia; + kitty = lib.getExe pkgs.kitty; + firefox = lib.getExe pkgs.firefox; + nautilus = lib.getExe pkgs.nautilus; + grim = lib.getExe pkgs.grim; + slurp = lib.getExe pkgs.slurp; + wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy"; + wpctl = "${pkgs.wireplumber}/bin/wpctl"; + brightnessctl = lib.getExe pkgs.brightnessctl; + playerctl = lib.getExe pkgs.playerctl; + ipc = target: "${noctalia} ipc call ${target}"; + + # a bind that also works on the lock screen (volume, brightness, media) + locked = cmd: _: { + props.allow-when-locked = true; + content.spawn-sh = cmd; + }; + workspaceBinds = lib.foldl' (acc: n: acc // { + "Mod+${toString n}".focus-workspace = n; + "Mod+Shift+${toString n}".move-column-to-workspace = n; + }) { } (lib.range 1 9); + in + { + packages.niri = inputs.wrapper-modules.wrappers.niri.wrap { + inherit pkgs; + settings = { + input = { + keyboard = { + xkb = { + layout = "us"; + options = "compose:caps,shift:both_capslock_cancel"; # as services.xserver.xkb + }; + }; + touchpad = { + tap = _: { }; + natural-scroll = _: { }; + }; + focus-follows-mouse = _: { }; + }; + + layout = { + gaps = 8; + focus-ring = { + width = 2; + active-color = "#ebbcba"; + inactive-color = "#26233a"; + }; + border.off = _: { }; + preset-column-widths = [ + { proportion = 0.33333; } + { proportion = 0.5; } + { proportion = 0.66667; } + ]; + }; + + prefer-no-csd = _: { }; + hotkey-overlay.skip-at-startup = _: { }; + xwayland-satellite.path = lib.getExe pkgs.xwayland-satellite; + + # The same session variables the Hyprland side sets (hosts/laptop configuration.nix). + environment = { + ELECTRON_OZONE_PLATFORM_HINT = "auto"; + QT_QPA_PLATFORM = "wayland;xcb"; + }; + + spawn-at-startup = [ noctalia ]; + + binds = { + # apps + "Mod+Return".spawn = [ kitty ]; + "Mod+Shift+Return".spawn = [ firefox ]; + "Mod+Shift+B".spawn = [ firefox ]; + "Mod+Shift+Alt+B".spawn = [ firefox "--private-window" ]; + "Mod+Shift+F".spawn = [ nautilus "--new-window" ]; + "Mod+Shift+O".spawn-sh = "obsidian"; # unfree, from the system profile (modules/features/workstation.nix) + "Mod+Shift+N".spawn-sh = "${kitty} -e \${EDITOR:-nano}"; + + # Noctalia + "Mod+Space".spawn-sh = ipc "launcher toggle"; + "Mod+Alt+Space".spawn-sh = ipc "launcher toggle"; + "Mod+Escape".spawn-sh = ipc "sessionMenu toggle"; + "Mod+Ctrl+P".spawn-sh = ipc "sessionMenu toggle"; + "Mod+A".spawn-sh = ipc "controlCenter toggle"; + "Mod+Comma".spawn-sh = ipc "notifications clear"; + "Mod+Ctrl+V".spawn-sh = ipc "launcher clipboard"; + "Mod+Ctrl+Space".spawn-sh = ipc "wallpaper toggle"; + "Mod+Shift+Escape".spawn-sh = ipc "lockScreen lock"; + + # windows and columns + "Mod+Q".close-window = _: { }; + "Mod+F".maximize-column = _: { }; + "Mod+G".fullscreen-window = _: { }; + "Mod+Shift+V".toggle-window-floating = _: { }; + "Mod+H".focus-column-left = _: { }; + "Mod+J".focus-window-down = _: { }; + "Mod+K".focus-window-up = _: { }; + "Mod+L".focus-column-right = _: { }; + "Mod+Left".focus-column-left = _: { }; + "Mod+Down".focus-window-down = _: { }; + "Mod+Up".focus-window-up = _: { }; + "Mod+Right".focus-column-right = _: { }; + "Mod+Shift+H".move-column-left = _: { }; + "Mod+Shift+J".move-window-down = _: { }; + "Mod+Shift+K".move-window-up = _: { }; + "Mod+Shift+L".move-column-right = _: { }; + "Mod+Shift+Left".move-column-left = _: { }; + "Mod+Shift+Down".move-window-down = _: { }; + "Mod+Shift+Up".move-window-up = _: { }; + "Mod+Shift+Right".move-column-right = _: { }; + "Mod+Ctrl+H".set-column-width = "-5%"; + "Mod+Ctrl+L".set-column-width = "+5%"; + "Mod+Ctrl+J".set-window-height = "-5%"; + "Mod+Ctrl+K".set-window-height = "+5%"; + "Mod+WheelScrollDown" = _: { + props.cooldown-ms = 150; + content.focus-workspace-down = _: { }; + }; + "Mod+WheelScrollUp" = _: { + props.cooldown-ms = 150; + content.focus-workspace-up = _: { }; + }; + "Mod+O".toggle-overview = _: { }; + "Mod+Shift+Slash".show-hotkey-overlay = _: { }; + + # screenshots + "Print".spawn-sh = "${grim} -g \"$(${slurp})\" - | ${wl-copy}"; + "Shift+Print".spawn-sh = "${grim} - | ${wl-copy}"; + + # media and hardware keys, also on the lock screen + "XF86AudioRaiseVolume" = locked "${wpctl} set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%+"; + "XF86AudioLowerVolume" = locked "${wpctl} set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%-"; + "XF86AudioMute" = locked "${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle"; + "XF86AudioMicMute" = locked "${wpctl} set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; + "XF86MonBrightnessUp" = locked "${brightnessctl} set 5%+"; + "XF86MonBrightnessDown" = locked "${brightnessctl} set 5%-"; + "XF86AudioPlay" = locked "${playerctl} play-pause"; + "XF86AudioPause" = locked "${playerctl} play-pause"; + "XF86AudioNext" = locked "${playerctl} next"; + "XF86AudioPrev" = locked "${playerctl} previous"; + + "Mod+Shift+E".quit = _: { }; # niri asks for confirmation + } // workspaceBinds; + }; + }; + }; + + flake.nixosModules.desktop-niri = + { config, lib, pkgs, ... }: + { + config = lib.mkIf config.daemon.desktop.niri.enable { + # nixpkgs' module registers the session for tuigreet and adds the + # GNOME portal Niri documents; the package is the wrapped one above. + programs.niri = { + enable = true; + package = self.packages.${pkgs.stdenv.hostPlatform.system}.niri; + }; + # Noctalia's battery widget reads UPower (nixpkgs' module leaves it off). + services.upower.enable = true; + # nixpkgs' portal config for niri names the gtk implementations for + # Access, FileChooser and Notification; keep that portal installed even + # when the Hyprland side (which also adds it) is switched off. + xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; + }; + }; +} diff --git a/modules/features/desktop/noctalia.nix b/modules/features/desktop/noctalia.nix @@ -0,0 +1,70 @@ +# modules/features/desktop/noctalia.nix — Noctalia Shell (bar, launcher, +# notifications, control centre, lock screen, wallpaper) as a wrapped, +# portable package: `nix run ~/NixDaemon#noctalia`. Niri spawns it at startup +# (niri.nix). Rosé Pine Main is Noctalia's own "Rosepine" scheme (dark half = +# base #191724, rose #ebbcba); never Moon. +# +# The settings below live in the Nix store (NOCTALIA_SETTINGS_FILE), so the +# GUI settings panel (right-click the bar) changes them for the running +# session only. To keep a change: tweak it in the GUI, then +# +# dump-noctalia-shell (in the same package) prints the live settings as Nix +# +# and copy the keys that differ from Noctalia's defaults into `settings` here. +{ inputs, ... }: +{ + perSystem = + { pkgs, ... }: + { + packages.noctalia = inputs.wrapper-modules.wrappers.noctalia-shell.wrap { + inherit pkgs; + settings = { + # Noctalia's current settings schema (Commons/Settings.qml). Without + # it every migration since v0 runs at start, one of them against + # ~/.config/noctalia. Bump it when noctalia-shell is updated + # (`dump-noctalia-shell` prints the value it expects). + settingsVersion = 59; + colorSchemes = { + predefinedScheme = "Rosepine"; + darkMode = true; + useWallpaperColors = false; # never derive colours from the wallpaper + }; + bar = { + position = "top"; + density = "compact"; + }; + general = { + lockOnSuspend = true; + telemetryEnabled = false; + }; + ui = { + fontDefault = "Noto Sans"; + fontFixed = "DMMono Nerd Font"; + }; + wallpaper = { + enabled = true; + # the same Rosé Pine wallpapers Caelestia uses (modules/home/caelestia.nix): + # three sets in subfolders, so the picker searches recursively + directory = "/home/daemonsec/Pictures/Wallpapers"; + viewMode = "recursive"; + fillMode = "crop"; + }; + location = { + name = "Douglas, Isle of Man"; # dashboard weather, as in Caelestia + useFahrenheit = false; + use12hourFormat = false; + }; + appLauncher = { + terminalCommand = "kitty -e"; + enableClipboardHistory = true; # the Ctrl+Super+V bind (niri.nix) opens this tab; needs cliphist + wl-paste (modules/home/session.nix) + }; + idle = { + enabled = true; + lockTimeout = 600; # Noctalia's own lock screen after ten idle minutes + screenOffTimeout = 1200; # as the Hyprland side (20 min) + suspendTimeout = 0; # never: Noctalia's default is 30 min, and NVIDIA suspend is untested here (nvidia.nix) + }; + }; + }; + }; +} diff --git a/modules/features/desktop/options.nix b/modules/features/desktop/options.nix @@ -0,0 +1,29 @@ +# modules/features/desktop/options.nix — the desktop switches. +# +# Both desktops are installed by default and chosen at login (tuigreet lists +# every session and remembers the last one). Set one to false in +# modules/hosts/laptop/configuration.nix to drop it from the system entirely. +# +# daemon.desktop.hyprland.enable Hyprland (uwsm) + Caelestia Shell +# daemon.desktop.niri.enable Niri + Noctalia Shell +# +# Home-manager modules read the same switches through `osConfig`. +{ ... }: +{ + flake.nixosModules.desktop-options = + { lib, config, ... }: + { + options.daemon.desktop = { + hyprland.enable = lib.mkEnableOption "Hyprland with Caelestia Shell" // { default = true; }; + niri.enable = lib.mkEnableOption "Niri with Noctalia Shell" // { default = true; }; + }; + + config.assertions = [ + { + assertion = config.daemon.desktop.hyprland.enable || config.daemon.desktop.niri.enable; + message = "daemon.desktop: enable at least one desktop"; + } + ]; + } + ; +} diff --git a/modules/features/home-manager.nix b/modules/features/home-manager.nix @@ -0,0 +1,27 @@ +# modules/features/home-manager.nix — home-manager as a NixOS module: the +# user's home (modules/home/default.nix) is built and activated by the same +# `nh os switch` as the system. There is no standalone home-manager profile. +{ self, inputs, ... }: +{ + flake.nixosModules.home-manager = + { user, ... }: + { + imports = [ inputs.home-manager.nixosModules.home-manager ]; + + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + backupFileExtension = "hm-bak"; + extraSpecialArgs = { inherit inputs user; }; + # The Hyprland and Caelestia home-manager modules only declare options; + # modules/home/{hyprland,caelestia}.nix decide whether they do anything + # (daemon.desktop.hyprland.enable). + sharedModules = [ + inputs.hyprland.homeManagerModules.default + inputs.caelestia-shell.homeManagerModules.default + ]; + users.${user} = self.homeModules.daemonsec; + }; + } + ; +} diff --git a/modules/features/workstation.nix b/modules/features/workstation.nix @@ -0,0 +1,48 @@ +# Claude Code, the Claude desktop app, Obsidian, and the git / GitHub / GitLab +# command-line tools. Written by nixdaemon-bootstrap.sh; edit freely. +{ ... }: +{ + flake.nixosModules.workstation = + { lib, pkgs, inputs, ... }: + let + # Anthropic's own Linux builds, repackaged for Nix by numtide and refreshed + # daily. (NixOS isn't a distro Anthropic supports directly: its desktop app + # ships as a .deb for Debian/Ubuntu.) + claude = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}; + in + { + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + # Unfree packages (Obsidian) are already allowed elsewhere in this config. + + environment.systemPackages = [ + claude.claude-code # terminal: `claude` + claude.claude-desktop # desktop app: "Claude" in your launcher, or `claude-desktop` + pkgs.obsidian + pkgs.git + pkgs.gh # GitHub CLI + pkgs.glab # GitLab CLI + pkgs.qemu_kvm # only for the desktop app's Cowork tab + ]; + + # The desktop app keeps its sign-in in the system keyring; without one it + # asks you to log in on every launch. + services.gnome.gnome-keyring.enable = lib.mkDefault true; + + # The desktop app's Cowork tab runs its tasks in a local VM, which needs KVM. + # If you don't use Cowork, delete these two lines and qemu_kvm above. + boot.kernelModules = [ "vhost_vsock" ]; + users.groups.kvm.members = [ "daemonsec" ]; + + # Run Electron apps (Claude, Obsidian) natively on Wayland, e.g. on Hyprland. + environment.sessionVariables.NIXOS_OZONE_WL = lib.mkDefault "1"; + + # Optional: numtide's binary cache, so the Claude packages download instead + # of building a couple of small helper tools locally on each update. + # nix.settings.extra-substituters = [ "https://cache.numtide.com" ]; + # nix.settings.extra-trusted-public-keys = [ + # "niks3.numtide.com-1:DTx8wZduET09hRmMtKdQDxNNthLQETkc/yaX7M4qK0g=" + # ]; + } + ; +} diff --git a/modules/home/caelestia.nix b/modules/home/caelestia.nix @@ -0,0 +1,262 @@ +# modules/home/caelestia.nix — Caelestia Shell as bar, launcher, notifications, +# OSD, lock and session menu; its CLI; the static Rosé Pine (M3) schemes. +# +# Look (2026-10-08): Rosé Pine dark (main, not moon) everywhere, translucent +# shell layers over blur, and Caelestia's own framed bar: the screen edge is a +# 10 px frame with 25 px rounded inner corners, the clock and tray sit in pill +# backgrounds, occupied workspaces are filled, the Nix snowflake is the logo. +# The sidebar, utilities and notification panels are narrower than stock +# (./caelestia/shell-tokens.json: 340 / 340 / 360 px instead of 430). +# A Rosé Pine Dawn mapping is registered too: caelestia scheme set -n rose-pine -f rose-pine-dawn +# and back: caelestia scheme set -n rose-pine -f rose-pine-dark +# +{ ... }: +{ + flake.homeModules.caelestia = + { config, pkgs, lib, inputs, osConfig, ... }: + let + system = pkgs.stdenv.hostPlatform.system; + hyprPkg = inputs.hyprland.packages.${system}.hyprland; + + # The CLI knows schemes by name and re-reads their colours whenever the + # wallpaper changes, so the two hand-mapped Rosé Pine → M3 palettes are + # registered as a real scheme (name rose-pine; flavours rose-pine-dark, mode + # dark, and rose-pine-dawn, mode light). The CLI's own `rosepine/dawn` is a + # Material palette generated from a gold seed, not the Rosé Pine colours. + cli = (inputs.caelestia-cli.packages.${system}.default).overrideAttrs (old: { + patchPhase = (old.patchPhase or "") + '' + install -Dm644 ${./caelestia/rose-pine-dark.txt} src/caelestia/data/schemes/rose-pine/rose-pine-dark/dark.txt + install -Dm644 ${./caelestia/rose-pine-dawn.txt} src/caelestia/data/schemes/rose-pine/rose-pine-dawn/light.txt + ''; + }); + + shell = + ((inputs.caelestia-shell.packages.${system}.with-cli).override { + caelestia-cli = cli; + hyprland = hyprPkg; # hyprctl from the same Hyprland the compositor runs + }).overrideAttrs (old: { + # bar.activeWindow.compact shows the program (desktop-entry name for the + # window class) instead of the window title. Upstream's compact mode only + # trims the title at its last " - ". Rebuilds the shell locally. + patches = (old.patches or [ ]) ++ [ ./caelestia/active-window-program-name.patch ]; + }); + + # ~/Pictures/Wallpapers/{rose-pine,rose-pine-dark,rose-pine-mid-walls}; the picker scans subfolders. + wallpaperDir = "${config.home.homeDirectory}/Pictures/Wallpapers"; + in + # Only when the Hyprland desktop is switched on (modules/features/desktop/options.nix). + lib.mkIf osConfig.daemon.desktop.hyprland.enable { + programs.caelestia = { + enable = true; + package = shell; + + # The module's default target is graphical-session.target, which Niri + # starts too (niri.service BindsTo it), so Caelestia would come up beside + # Noctalia in a Niri login. uwsm's Hyprland session target only exists + # under Hyprland. + systemd.target = "wayland-session@hyprland.desktop.target"; + + cli = { + enable = true; + package = cli; + # Theming is static Rosé Pine from this repo; the CLI must not rewrite + # kitty, GTK, Hyprland or anything else when the wallpaper changes. + settings.theme = { + enableTerm = false; + enableHypr = false; + enableDiscord = false; + enableSpicetify = false; + enablePandora = false; + enableFuzzel = false; + enableBtop = false; + enableNvtop = false; + enableHtop = false; + enableGtk = false; + enableQt = false; + enableWarp = false; + enableChromium = false; + enableZed = false; + enableCava = false; + }; + }; + + settings = { + appearance = { + font = { + clock = "Rubik"; + workspaces = "Rubik"; + headline.family = "Noto Sans"; + title.family = "Noto Sans"; + body.family = "Noto Sans"; + label.family = "Noto Sans"; + mono.family = "DMMono Nerd Font"; + }; + anim.durations.scale = 1.15; + transparency = { + enabled = true; + base = 0.85; + layers = 0.4; + }; + }; + general = { + # The Nix snowflake in the bar, dashboard and lock screen (empty = Caelestia's own logo). + logo = "/run/current-system/sw/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + apps = { + terminal = [ "kitty" ]; + audio = [ "caelestia" "shell" "drawers" "toggle" "sidebar" ]; # was the Omarchy audio popup + playback = [ "mpv" ]; + explorer = [ "nautilus" ]; + }; + idle = { + lockBeforeSleep = false; + inhibitWhenAudio = true; + # Lock after 15 min idle, screen off after 20; audio playing or a + # fullscreen app with an idle inhibitor holds both off. + timeouts = [ + { timeout = 900; idleAction = "lock"; respectInhibitors = true; } + { timeout = 1200; idleAction = "dpms off"; returnAction = "dpms on"; } + ]; + }; + }; + # Omarchy drew the wallpaper; here Caelestia does. Colours stay static. + background = { + enabled = true; + wallpaperEnabled = true; + # Big clock on the wallpaper, bottom right, with a soft shadow. + desktopClock = { + enabled = true; + position = "bottom-right"; + scale = 1.0; + shadow.enabled = true; + }; + # Audio visualiser along the bottom of the wallpaper while something plays (cava is built in). + visualiser = { + enabled = true; + autoHide = true; + blur = false; + rounding = 1; + spacing = 1; + }; + }; + bar = { + persistent = true; + showOnHover = true; + workspaces = { + shown = 5; + activeIndicator = true; + occupiedBg = true; # filled pills behind workspaces that have windows + showWindows = true; + activeTrail = true; + }; + activeWindow = { + compact = true; # the program's name (patched, see `shell` above), not the title + inverted = true; # on a primary-coloured pill + }; + clock = { + showDate = true; + showIcon = true; + background = true; # clock in its own pill + }; + tray = { + background = true; # tray in its own pill + recolour = true; # tray icons tinted to the scheme + }; + statusIcons = [ + { id = "lockStatus"; enabled = true; } + { id = "audio"; enabled = true; } + { id = "microphone"; enabled = true; } # shows when something is capturing + { id = "kbLayout"; enabled = false; } + { id = "network"; enabled = true; } + { id = "bluetooth"; enabled = true; } + { id = "battery"; enabled = true; } + ]; + }; + # Caelestia's frame: the bar is one side of a border around the screen + # whose inner corners are rounded. Stock values; the carried 5/0/0 was a flat strip. + border = { + thickness = 10; + rounding = 25; + smoothing = 20; + }; + dashboard = { + enabled = true; + showWeather = true; + performance.showGpu = true; + }; + launcher = { + enabled = true; + maxShown = 8; + vimKeybinds = true; # ctrl+j/k move, like everywhere else here + actions = [ + { name = "Calculator"; icon = "calculate"; description = "Do simple math equations (powered by Qalc)"; command = [ "autocomplete" "calc" ]; enabled = true; dangerous = false; } + { name = "Wallpaper"; icon = "image"; description = "Pick a wallpaper"; command = [ "caelestia" "wallpaper" ]; enabled = true; dangerous = false; } + { name = "Lock"; icon = "lock"; description = "Lock the session"; command = [ "caelestia" "shell" "lock" "lock" ]; enabled = true; dangerous = false; } + { name = "Sleep"; icon = "bedtime"; description = "Suspend"; command = [ "systemctl" "suspend" ]; enabled = true; dangerous = false; } + { name = "Settings"; icon = "settings"; description = "Configure the shell"; command = [ "caelestia" "shell" "nexus" "open" ]; enabled = true; dangerous = false; } + { name = "Logout"; icon = "exit_to_app"; description = "Log out of the current session"; command = [ "uwsm" "stop" ]; enabled = true; dangerous = true; } + { name = "Reboot"; icon = "cached"; description = "Reboot the system"; command = [ "systemctl" "reboot" ]; enabled = true; dangerous = true; } + { name = "Shutdown"; icon = "power_settings_new"; description = "Shutdown the system"; command = [ "systemctl" "poweroff" ]; enabled = true; dangerous = true; } + ]; + }; + lock = { + enabled = true; # Caelestia is the lock screen here (Omarchy had hyprlock) + useWallpaper = true; # the wallpaper behind the lock, not a flat colour + }; + notifs = { + expire = true; + defaultExpireTimeout = 6000; + actionOnClick = true; + }; + osd = { + enabled = true; + enableBrightness = true; + }; + services = { + gpuType = "Generic"; # must be a string + smartScheme = false; # never derive colours from the wallpaper + defaultPlayer = "rmpc"; + weatherLocation = "54.15,-4.48"; # Douglas, Isle of Man (dashboard weather card) + }; + session = { + enabled = true; + icons.hibernate = "bedtime"; + commands = { + logout = [ "uwsm" "stop" ]; + shutdown = [ "systemctl" "poweroff" ]; + hibernate = [ "systemctl" "suspend" ]; + reboot = [ "systemctl" "reboot" ]; + }; + }; + sidebar.enabled = true; + utilities = { + enabled = true; + toasts.nowPlaying = true; # a toast when the track changes + }; + paths.wallpaperDir = wallpaperDir; + }; + }; + + # The scheme the shell reads at start: Rosé Pine dark (scheme.json; + # scheme-dawn.json is the light mapping). `caelestia scheme set …` rewrites + # this file (home-manager backs the symlink up as .hm-bak when that happens). + home.file.".local/state/caelestia/scheme.json".source = ./caelestia/scheme.json; + + # Internal size tokens: the shell reads this file (defaults in its source, + # plugin/src/Caelestia/Config/tokens.hpp: sidebar, utilities and + # notification width 430, bar innerWidth 40). Here: sidebar and utilities + # 340, notifications 360, bar 36. Rounding, padding and spacing stay stock. + home.file.".config/caelestia/shell-tokens.json".source = ./caelestia/shell-tokens.json; + + # First wallpaper, only if none was ever chosen (Caelestia keeps the choice in state). + home.activation.caelestiaWallpaper = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + st="$HOME/.local/state/caelestia/wallpaper" + if [ ! -e "$st/path.txt" ]; then + wp=$(ls "${wallpaperDir}"/rose-pine-dark/*.png 2>/dev/null | head -1 || true) + if [ -n "$wp" ]; then + mkdir -p "$st" && printf '%s' "$wp" > "$st/path.txt" && ln -sfn "$wp" "$st/current" + fi + fi + ''; + } + ; +} diff --git a/home/caelestia/active-window-program-name.patch b/modules/home/caelestia/active-window-program-name.patch diff --git a/home/caelestia/rose-pine-dark.txt b/modules/home/caelestia/rose-pine-dark.txt diff --git a/home/caelestia/rose-pine-dawn.txt b/modules/home/caelestia/rose-pine-dawn.txt diff --git a/home/caelestia/scheme-dawn.json b/modules/home/caelestia/scheme-dawn.json diff --git a/home/caelestia/scheme.json b/modules/home/caelestia/scheme.json diff --git a/home/caelestia/shell-tokens.json b/modules/home/caelestia/shell-tokens.json diff --git a/modules/home/cheats.nix b/modules/home/cheats.nix @@ -0,0 +1,67 @@ +# modules/home/cheats.nix — the cheat cards this repo ships, in the house +# style (~/.local/bin/*-cheat, rendered with the toolbox's cheat-render). +# Every modules/home/cheats/<name>.md becomes a `<name>-cheat` command: +# +# nix-cheat rebuilding this machine: layout, nixos-rebuild, nh, add, desktop (Hyprland or Niri), secrets, repo +# gpg-cheat GnuPG from the key hierarchy to signing, encryption, SSH, git, offline primary, fixes +# niri-cheat Niri + Noctalia: every bind, Noctalia ipc, niri msg, wallpaper, settings, fixes +# +# <name>-cheat the whole card <name>-cheat --list the section names +# <name>-cheat SECTION one section <name>-cheat --raw the markdown itself +# +# Rendered with cheat-render when that is on PATH, else glow, else printed +# plain. These cards live here (not in the dotfiles) because they document +# this repo and this machine; xcheats only lists ~/.local/bin cards, so call +# these by name. +{ ... }: +{ + flake.homeModules.cheats = + { pkgs, lib, ... }: + let + cards = [ "nix" "gpg" "niri" ]; + + mkCheat = name: pkgs.writeShellScriptBin "${name}-cheat" '' + set -uo pipefail + card=${./cheats + "/${name}.md"} + + usage() { + echo "usage: ${name}-cheat [SECTION] [--list] [--raw] [--help]" + echo " sections: $(sections | tr '\n' ' ')" + } + sections() { # first word of each '## ' heading + ${pkgs.gnused}/bin/sed -n 's/^## \([a-z-]*\).*/\1/p' "$card" + } + section() { # the heading whose first word matches $1, up to the next heading + ${pkgs.gawk}/bin/awk -v want="$1" ' + /^## / { on = (tolower($2) == want) } + /^# / { next } + on + ' "$card" + } + render() { + if [ ! -t 1 ]; then cat + elif command -v cheat-render >/dev/null 2>&1; then cheat-render - | ''${PAGER:-less -R} + else ${pkgs.glow}/bin/glow -p - + fi + } + + case "''${1:-}" in + -h|--help) usage; exit 0 ;; + --list) sections; exit 0 ;; + --raw) cat "$card"; exit 0 ;; + "") render < "$card" ;; + *) + key=$(echo "$1" | tr '[:upper:]' '[:lower:]') + out=$(section "$key") + if [ -z "$out" ]; then + echo "${name}-cheat: no section '$1'" >&2; usage >&2; exit 1 + fi + { head -1 "$card"; echo; printf '%s\n' "$out"; } | render ;; + esac + ''; + in + { + home.packages = map mkCheat cards; + } + ; +} diff --git a/modules/home/cheats/gpg.md b/modules/home/cheats/gpg.md @@ -0,0 +1,321 @@ +# gpg — the key hierarchy, and every verb + +GnuPG **2.4.9** here. Pinentry is the GNOME dialog (NixOS `programs.gnupg.agent`), `GPG_TTY` is exported by core.zsh, +and **`gpgx`** (`gpgx-cheat`) is the no-flags wrapper for the daily lock/unlock/sign/verify. This card is the real thing underneath. +Terminal card: **`gpg-cheat`** — sections `model setup keygen subkeys backup import sign verify encrypt decrypt edit revoke ssh git keyservers trust inspect offline agent fix mine`. +`FPR` below = a full 40-hex fingerprint. Yours (the main key): `3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4`. + +## model — one primary key, several subkeys + +```text +primary key [C] certify = the identity. Signs your own subkeys and user IDs, and other people's keys. + Keep it OFFLINE once the subkeys exist; it only comes out to add/revoke subkeys or UIDs. +subkey [S] sign = signs files, commits, mail. Rotate yearly without touching the identity. +subkey [E] encrypt = what others encrypt TO. Rotating it does not re-encrypt old mail; keep old ones to read it. +subkey [A] authenticate = SSH login (gpg-agent as the ssh agent). +user ID "Name <mail>" = one per address; the primary one is what people see first. +``` + +- Capabilities show in `gpg -K` as `[SC]`, `[E]`, `[A]`. A primary with `[SC]` signs with the identity key itself, which is fine but not rotatable. +- The **fingerprint** (40 hex) is the key; the "key ID" is its last 16 (long) or 8 (short, collidable — never use). +- `sec` = secret primary present. `sec#` = primary absent (offline), only subkeys here. `ssb` = secret subkey. +- Ownertrust (how much you trust a key to vouch for others) is separate from validity (is this key really theirs). `[ultimate]` is your own. + +## setup — ~/.gnupg and the agent + +```sh +# ~/.gnupg/gpg.conf (create it; sane modern defaults) +keyid-format 0xlong +with-fingerprint +with-subkey-fingerprints +default-key 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 +default-recipient-self # `gpg -e file` encrypts to you when no -r given +personal-cipher-preferences AES256 AES192 AES +personal-digest-preferences SHA512 SHA384 SHA256 +cert-digest-algo SHA512 +no-emit-version +no-comments +keyserver hkps://keys.openpgp.org +auto-key-locate local,wkd # find a key by mail address (WKD) before asking a keyserver +trust-model tofu+pgp # remember first-seen keys per address, warn on change + +# ~/.gnupg/gpg-agent.conf +default-cache-ttl 3600 # seconds a passphrase stays cached after last use +max-cache-ttl 28800 # hard ceiling +# pinentry-program is set by NixOS (modules/hosts/laptop/configuration.nix: pinentryPackage = pinentry-gnome3) +``` + +```sh +chmod 700 ~/.gnupg # gpg refuses "unsafe permissions" otherwise +gpg-connect-agent reloadagent /bye # after editing gpg-agent.conf +gpgconf --kill gpg-agent # restart it entirely (it relaunches on demand) +gpg --version # algorithms available +``` + +## keygen — a proper key, the modern way + +Certify-only primary, then three subkeys. Ed25519/Curve25519 throughout (fast, small, the 2.4 default). + +```sh +gpg --quick-generate-key 'DAEMON-SEC <zer0sec.xp@icloud.com>' ed25519 cert 2y # primary: [C] only, 2-year expiry +FPR=$(gpg -K --with-colons 'zer0sec.xp@icloud.com' | awk -F: '$1=="fpr"{print $10; exit}') +gpg --quick-add-key "$FPR" ed25519 sign 1y # [S] +gpg --quick-add-key "$FPR" cv25519 encr 1y # [E] +gpg --quick-add-key "$FPR" ed25519 auth 1y # [A] (for SSH) +gpg -K --with-subkey-fingerprints # see the four, check capabilities and expiries +``` + +- Expiry is a **safety net**, not a deadline: extend it any time with the primary key (see *edit*). A key that expires with its owner gone is self-cleaning. +- Interactive version: `gpg --full-generate-key --expert` → `(11) ECC (set your own capabilities)` → toggle `S` off so only `Certify` remains → `Curve 25519`. Then add subkeys with `--quick-add-key` or in `--edit-key` → `addkey`. +- A passphrase protects the secret key *file*; the agent caches it. Use a long one; the pinentry dialog asks. +- The revocation certificate is written automatically to `~/.gnupg/openpgp-revocs.d/FPR.rev` — back it up with the key (see *backup*). + +## subkeys — add, rotate, drop + +```sh +gpg --quick-add-key FPR ed25519 sign 1y # new signing subkey (old one stays until you revoke it) +gpg --quick-set-expire FPR 1y '*' # extend EVERY subkey a year (`'*'` = all subkeys; a sub-fpr = that one) +gpg --quick-set-expire FPR 2y # extend the primary +gpg --edit-key FPR # interactive: `key 2` selects subkey 2 (the `*` marks it), then + # `expire` / `revkey` / `delkey` / `passwd` / `save` +gpg -K --with-colons | awk -F: '$1=="ssb"{print $12, $5}' # capability letters + key id of each subkey +``` + +Old encryption subkeys: **keep them** (revoked or expired, still in the ring) or you lose the ability to read what was sent to them. + +## backup — export, revocation, paper + +```sh +gpg --armor --export FPR > daemon-sec.pub.asc # public key: share freely +gpg --armor --export-secret-keys FPR > daemon-sec.SECRET.asc # everything: primary + subkeys (passphrase-protected) +gpg --armor --export-secret-subkeys FPR > daemon-sec.subkeys.asc # subkeys only (what a laptop should carry; see *offline*) +cp ~/.gnupg/openpgp-revocs.d/FPR.rev daemon-sec.rev # the revocation certificate +gpg --export-ownertrust > ownertrust.txt # your trust assignments +gpg --gen-revoke FPR > daemon-sec.rev # make a fresh revocation cert by hand +nix shell nixpkgs#paperkey -c sh -c 'gpg --export-secret-keys FPR | paperkey > daemon-sec.paper.txt' # printable secret bits +``` + +Where it goes: the vault (encrypted), never the dotfiles repo. The SECRET export + the .rev are the two files that matter. +The secret export is still encrypted with your passphrase; the passphrase is the third thing to keep. + +## import — keys, trust, restore + +```sh +gpg --import daemon-sec.pub.asc # someone's public key (or your own on a new machine) +gpg --import daemon-sec.SECRET.asc # restore: asks for the passphrase +gpg --import-ownertrust < ownertrust.txt +gpg --edit-key FPR trust # set ownertrust: 5 = ultimate (yours), 4 = full, 3 = marginal +gpg --lsign-key FPR # "I checked this key": local signature, never exported +gpg --sign-key FPR # exportable certification (web of trust) +gpg --show-keys someone.asc # look at a key file WITHOUT importing it +gpg --delete-keys FPR # forget a public key; --delete-secret-keys for the secret part first +``` + +On a fresh machine: import the SECRET file, `gpg --edit-key FPR trust` → `5`, done. Or import only the subkeys file for a laptop. + +## sign — files, text, commits + +```sh +gpg --detach-sign --armor file # file.asc beside it: the usual way to sign a release/file +gpg --detach-sign file # binary file.sig +gpg --clearsign message.txt # message.txt.asc: readable text with a signature block (mail, announcements) +gpg --sign file # file.gpg: compressed file + signature in one (recipient runs gpg -d) +gpg -u FPR --detach-sign file # pick the key (-u / --local-user); `FPR!` = that exact subkey +echo "text" | gpg --clearsign # from a pipe +gpg --sign --encrypt -r mail file # sign AND encrypt (see *encrypt*) +``` + +Signing uses the `[S]` subkey automatically (or the primary if it has S). The agent asks for the passphrase once per cache TTL. + +## verify — did this come from them, unchanged + +```sh +gpg --verify file.asc file # detached signature (.asc/.sig) + the file +gpg --verify file.sig # gpg finds `file` next to it +gpg --verify message.txt.asc # clearsigned text +gpg --decrypt file.gpg > file # inline-signed: extracts the file and prints the verification +gpg --verify --verbose file.asc file # which key, which subkey, when +``` + +Reading the result: **Good signature from "Name <mail>"** = cryptographically valid. The warning +`This key is not certified with a trusted signature` means you have not set ownertrust / signed their key +— the signature is still valid; the question is whether the key is really theirs. Compare the fingerprint +out-of-band once, then `gpg --lsign-key FPR` and the warning goes away. +`BAD signature` = the file changed or the signature is for another file. Exit code 0 only on Good. + +## encrypt — to people, to yourself, with a passphrase + +```sh +gpg --encrypt --recipient mail@example.com --armor file # file.asc, readable only by that key +gpg -e -r mail@example.com -r zer0sec.xp@icloud.com file # several recipients (add yourself to read it later!) +gpg -e file # to yourself (default-recipient-self in gpg.conf) +gpg -se -r mail file # sign + encrypt: they know it is from you +gpg --symmetric --cipher-algo AES256 file # passphrase only, no keys (file.gpg); shares via a channel you trust +gpg -c --armor file # same, armored +gpg -o out.gpg -e -r mail file # choose the output name +tar cz directory | gpg -e -r mail > directory.tgz.gpg # a whole directory, through a pipe +gpg --hidden-recipient mail -e file # do not reveal who it is for (-R) +gpg --encrypt-to FPR # in gpg.conf: ALWAYS add this recipient (yourself) silently +``` + +- `--armor` (`-a`) makes ASCII text you can paste into mail; without it the output is binary (smaller). +- Encrypt uses the recipient's `[E]` subkey. "No public key" = you have not imported theirs; "unusable public key" = theirs expired. +- Symmetric + a strong passphrase is fine for backups and for sending to someone with no key. + +## decrypt — and what to do when it fails + +```sh +gpg --decrypt file.gpg > file # -d: to stdout +gpg -o file -d file.gpg # to a named file +gpg file.gpg # guesses: decrypts (or verifies) and writes `file` +gpg --decrypt-files *.gpg # many at once, each to its name without .gpg +gpg -d file.gpg | tar xz # straight into tar +gpg --list-packets file.gpg # WHO can decrypt this: the key IDs it was encrypted to, the algorithms +``` + +"decryption failed: No secret key" = it was not encrypted to any key you hold (check with `--list-packets`; +compare with `gpg -K`). On a laptop with subkeys only, that is fine as long as the `[E]` subkey is there. + +## edit — identities, passphrase, expiry + +```sh +gpg --quick-add-uid FPR 'DAEMON-SEC <zer0sec.xp@icloud.com>' # add a user ID (fix a name, add an address) +gpg --quick-set-primary-uid FPR 'DAEMON-SEC <zer0sec.xp@icloud.com>' +gpg --quick-revoke-uid FPR 'DAMEON-NIX <zer0sec.xp@icloud.com>' # retire a UID (keys cannot delete published UIDs; revoke them) +gpg --change-passphrase FPR # new passphrase for the secret key +gpg --quick-set-expire FPR 2y # primary expiry; `0` = never +gpg --quick-set-expire FPR 1y '*' # all subkeys +gpg --edit-key FPR # the interactive editor; `help` lists everything: +# uid N / key N select · adduid deluid revuid primary · addkey delkey revkey expire · passwd · trust · save / quit +``` + +Every change to UIDs or subkeys needs the **primary** secret key present (`sec`, not `sec#`). Then re-export the public key: others must re-import it to see the change. + +## revoke — when a key is lost or compromised + +```sh +gpg --import daemon-sec.rev # the stored revocation certificate: marks YOUR key revoked locally +gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish the revocation so others see it +gpg --edit-key FPR → key 2 → revkey → save # revoke ONE subkey (compromised laptop: revoke its subkeys, keep the primary) +gpg --quick-revoke-uid FPR 'uid string' # revoke an identity +``` + +Revoked ≠ deleted: the key stays in the ring so old signatures still verify (as "made with a revoked key") and old mail still decrypts. +If the laptop is lost and you kept the primary offline: revoke its subkeys, make new ones, publish. The identity survives. + +## ssh — the [A] subkey as your SSH key + +```sh +# modules/hosts/laptop/configuration.nix: programs.gnupg.agent.enableSSHSupport = true; # gpg-agent becomes the ssh agent (SSH_AUTH_SOCK) +gpg -K --with-keygrip # the keygrip of the [A] subkey +echo KEYGRIP >> ~/.gnupg/sshcontrol # tell the agent to serve it +gpg --export-ssh-key FPR # the public key in authorized_keys format +gpg --export-ssh-key FPR >> ~/.ssh/authorized_keys_for_that_host # paste it where you log in +ssh-add -L # the agent now lists it +``` + +Your file key `~/.ssh/id_ed25519` keeps working next to it; ssh tries agent keys first. Use one or the other per host in `~/.ssh/config` (`IdentitiesOnly yes` + `IdentityFile`). + +## git — signed commits and tags + +```sh +git config --global gpg.format openpgp +git config --global user.signingkey 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 # or a subkey fpr with `!` to force it +git config --global commit.gpgsign true # every commit +git config --global tag.gpgSign true +git commit -S -m "msg" # one-off when gpgsign is off +git tag -s v1.0 -m "release" # signed tag; git tag -v v1.0 verifies +git log --show-signature -3 # see who signed what +git verify-commit HEAD +gpg --armor --export FPR | wl-copy # paste into GitHub/GitLab → Settings → GPG keys, so they show "Verified" +``` + +jj: `jj config set --user signing.behavior own` + `signing.backend "gpg"` + `signing.key FPR` signs the commits jj makes. + +## keyservers — publishing and finding keys + +```sh +gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish; keys.openpgp.org mails you to verify the address before it shows the UID +gpg --keyserver hkps://keys.openpgp.org --recv-keys FPR # fetch by fingerprint (the only safe way to fetch) +gpg --locate-keys someone@example.com # WKD: their domain serves the key (auto-key-locate in gpg.conf) +gpg --search-keys 'name' # interactive search; verify the fingerprint with them before trusting +gpg --refresh-keys # pull revocations/expiry updates for every key you hold +``` + +## trust — validity versus ownertrust + +- A signature is **valid** when the key is valid. A key is valid when *you* certified it (`--sign-key` / `--lsign-key`) or enough trusted people did. +- **Ownertrust** (`--edit-key FPR trust`, 1-5) is how much you let that person's certifications count for *other* keys. Give 5 only to yourself. +- `trust-model tofu+pgp`: the first key seen for an address is remembered; a different one later triggers a warning. `gpg --tofu-policy good FPR` to accept a change. +- `gpg --check-signatures FPR` lists who certified a key. `gpg --update-trustdb` recomputes validity after trust changes. + +## inspect — what is this thing + +```sh +gpg -k # public keys (--list-keys); gpg -K = secret keys +gpg -k --with-subkey-fingerprints --with-keygrip FPR +gpg --fingerprint FPR # just the fingerprint, formatted for reading aloud +gpg --list-packets file.gpg # structure of any OpenPGP file: recipients, algorithms, signature dates +gpg --show-keys key.asc # describe a key file without importing +gpg -k --with-colons FPR # machine-readable (pub/sub/uid/fpr records) +gpg --card-status # a YubiKey/OpenPGP card, if one is plugged in +gpg --export FPR | gpg --list-packets | grep -A2 'signature packet' # certifications on your key +``` + +## offline — primary key off the laptop + +The point of subkeys: the laptop carries only `[S] [E] [A]`; the primary (the identity) lives on encrypted offline storage. + +```sh +gpg --armor --export-secret-keys FPR > daemon-sec.SECRET.asc # 1. full backup first (vault, encrypted USB) +gpg --armor --export-secret-subkeys FPR > subkeys.asc # 2. the laptop's share +gpg --delete-secret-keys FPR # 3. remove everything secret here +gpg --import subkeys.asc # 4. put the subkeys back +gpg -K # shows `sec#` = primary absent, `ssb` present: correct +``` + +To add a subkey / extend expiry / sign someone's key later: import the SECRET backup in a temporary `GNUPGHOME`, do the change, export the public key and subkeys again, wipe the temp dir: +```sh +export GNUPGHOME=$(mktemp -d); gpg --import daemon-sec.SECRET.asc; gpg --quick-set-expire FPR 2y '*' +gpg --armor --export FPR > pub.asc; gpg --armor --export-secret-subkeys FPR > subkeys.asc; rm -rf "$GNUPGHOME"; unset GNUPGHOME +gpg --import pub.asc subkeys.asc # back in the normal ring +``` +A YubiKey does the same with the subkeys moved onto the card: `gpg --edit-key FPR` → `key N` → `keytocard`. + +## agent — passphrase caching, pinentry + +```sh +gpg-connect-agent 'KEYINFO --list' /bye # which keys are cached right now (the `1` column) +gpg-connect-agent reloadagent /bye # re-read gpg-agent.conf +gpgconf --kill gpg-agent # forget every cached passphrase now +gpg --pinentry-mode loopback -d file.gpg # type the passphrase in the terminal instead of the dialog (what gpgx does) +echo test | gpg --clearsign >/dev/null # cheap way to pre-unlock the key for a batch of operations +``` + +`export GPG_TTY=$(tty)` must be set for terminal pinentries (core.zsh does it). The dialog here is pinentry-gnome3. + +## fix — the usual errors + +- `Inappropriate ioctl for device` → `export GPG_TTY=$(tty)` (or the session has no tty: use `--pinentry-mode loopback`). +- `No secret key` on decrypt → not encrypted to you: `gpg --list-packets file.gpg` shows the key IDs it was made for. +- `unusable public key` / `unusable secret key` → expired or revoked (sub)key: `gpg -k FPR`, then `--quick-set-expire` (yours) or ask for a fresh key (theirs). +- `There is no assurance this key belongs to the named user` → ownertrust: `gpg --lsign-key FPR` after checking the fingerprint, or `--trust-model always` for one command. +- `gpg: WARNING: unsafe permissions on homedir` → `chmod 700 ~/.gnupg; chmod 600 ~/.gnupg/*`. +- Commands hang → a stale lock or a stuck daemon: `gpgconf --kill all`, `rm -f ~/.gnupg/*.lock ~/.gnupg/public-keys.d/*.lock`. +- `signing failed: No pinentry` → agent cannot reach a pinentry: `gpgconf --kill gpg-agent` and retry from a terminal with `GPG_TTY` set. +- Git: `error: gpg failed to sign the data` → almost always the two above; `GIT_TRACE=1 git commit` shows gpg's own message. +- Key shows `[expired]` on the other side after you extended it → they need your re-exported public key (or `--refresh-keys`). + +## mine — this machine, today + +- **Main key** `3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4`, UID `daemon (Main_Key) <zer0sec.xp@icloud.com>`, + RSA 4096 made 2025-12-30, imported from the vault on 2026-10-08: primary `[SC]` + one `[E]` subkey, **no expiry**, + ownertrust ultimate, `default-key` in gpg.conf, git signs with it (`commit.gpgsign`, `tag.gpgSign` on). + No revocation certificate is stored here yet: `gpg --gen-revoke 3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4 > daemon-main.rev` + and put it in the vault. Optional upgrades, all without changing the identity: an expiry (`--quick-set-expire`), + a `[S]` signing subkey and an `[A]` auth subkey (`--quick-add-key … rsa4096 sign|auth`), the primary offline (*offline*). +- **Spare** `E989B40F2382569DD6080336BF684D91DF095E48` (`DAMEON-NIX`, ed25519, made 2026-10-08, never published): + delete it with `gpg --delete-secret-and-public-keys E989B40F2382569DD6080336BF684D91DF095E48`, or keep it as a scratch key. +- Pinentry: GNOME dialog (modules/hosts/laptop/configuration.nix). `gpgx` uses loopback so the passphrase is typed in the terminal instead. +- Secrets in the NixDaemon repo are **age**, not gpg (`nix-cheat secrets`); the vault's `ssd.key.gpg` is gpg-encrypted, decrypt it with `gpg -d`. +- `gpgx-cheat` is the wrapper's card; `gpg-cheat SECTION` prints one section of this one. diff --git a/modules/home/cheats/niri.md b/modules/home/cheats/niri.md @@ -0,0 +1,196 @@ +# niri — Niri + Noctalia, every key and command + +Niri **26.04** (scrolling columns: windows sit in columns on an endless strip, workspaces stack vertically) with +**Noctalia 4.7.7** as bar, launcher, notifications, lock screen and wallpaper. Both are wrapped packages built from +`modules/features/desktop/niri.nix` and `noctalia.nix`; Rosé Pine Main throughout. +Terminal card: **`niri-cheat`** — sections `model apps shell windows move resize workspaces media screenshots ipc msg wallpaper settings session fix`. +`Mod` = **Super** in a real login, **Alt** when nested via `nix run ~/NixDaemon#niri`. + +## model — columns, windows, workspaces + +```text +workspace one per row; Mod+1..9 or the mouse wheel moves between them. Always one empty one at the bottom. +column the unit you scroll through left/right. New windows open as a new column to the right. +window a column holds one or more windows stacked vertically (Mod+J/K moves between them). +overview Mod+O zooms out over every workspace; click or arrow to pick, Mod+O again to leave. +``` + +- Nothing is ever "off screen" in a bad way: the strip just scrolls. Mod+H/L to walk it. +- Column widths come from presets (⅓, ½, ⅔); Mod+F maximises one column, Mod+G is true fullscreen. + +## apps — launch + +```text +Mod+Return kitty +Mod+Shift+Return firefox Mod+Shift+B firefox +Mod+Shift+Alt+B firefox private window +Mod+Shift+F nautilus (new window) +Mod+Shift+O obsidian +Mod+Shift+N kitty running $EDITOR (nano if unset) +``` + +## shell — Noctalia keys + +```text +Mod+Space launcher (apps; type > for commands) Mod+Alt+Space same +Mod+Ctrl+V launcher → clipboard history (cliphist) +Mod+A control centre (wifi, bluetooth, volume, settings gear) +Mod+Escape session menu (lock / suspend / logout / reboot / shutdown) Mod+Ctrl+P same +Mod+Shift+Escape lock now +Mod+Comma clear notifications +Mod+Ctrl+Space wallpaper picker +right-click the bar Noctalia settings +``` + +## windows — focus and state + +```text +Mod+H / Mod+Left focus column left +Mod+L / Mod+Right focus column right +Mod+J / Mod+Down focus window below (same column) +Mod+K / Mod+Up focus window above +Mod+Q close window +Mod+F maximise column (toggle) +Mod+G fullscreen window (toggle) +Mod+Shift+V toggle floating +Mod+O overview +Mod+Shift+/ niri's own hotkey overlay (the full live list) +``` + +Focus follows the mouse. + +## move — rearrange + +```text +Mod+Shift+H / Shift+Left move column left +Mod+Shift+L / Shift+Right move column right +Mod+Shift+J / Shift+Down move window down (within / out of the column) +Mod+Shift+K / Shift+Up move window up +Mod+Shift+1..9 move column to workspace N +``` + +## resize — width and height + +```text +Mod+Ctrl+H column width −5% Mod+Ctrl+L column width +5% +Mod+Ctrl+J window height −5% Mod+Ctrl+K window height +5% +``` + +Not bound (yet): cycling the ⅓/½/⅔ presets. `niri msg action switch-preset-column-width` does it from a shell. + +## workspaces + +```text +Mod+1..9 focus workspace N +Mod+Shift+1..9 move focused column to workspace N +Mod+WheelDown / Up next / previous workspace (150 ms cooldown) +``` + +## media — hardware keys (also work on the lock screen) + +```text +XF86AudioRaiseVolume / Lower volume ±5% (cap 140%) XF86AudioMute / MicMute toggle +XF86MonBrightnessUp / Down brightness ±5% +XF86AudioPlay / Pause play-pause XF86AudioNext / Prev track +``` + +## screenshots + +```text +Print select a region → clipboard (grim + slurp) +Shift+Print whole screen → clipboard +``` + +Save the clipboard to a file: `wl-paste > ~/Pictures/shot.png`. + +## ipc — drive Noctalia from a shell + +The wrapped Noctalia is not on PATH yet, so go through the flake: + +```sh +noct() { nix run ~/NixDaemon#noctalia -- ipc call "$@"; } # put in a zsh file to keep it + +noct settings toggle # settings window noct settings openTab <tab> +noct launcher toggle # also: clipboard emoji windows command +noct controlCenter toggle +noct sessionMenu toggle # also: lock lockAndSuspend +noct lockScreen lock +noct notifications toggleHistory # also: toggleDND clear dismissAll +noct idleInhibitor toggle # keep the screen awake enableFor 3600 +noct nightLight toggle +noct darkMode toggle # setDark / setLight +noct volume increase # decrease muteOutput muteInput togglePanel +noct brightness set 50 +noct wifi toggle # bluetooth toggle airplaneMode toggle +noct powerProfile cycle # set performance|balanced|power-saver +noct media playPause # next previous seekRelative 10 +noct bar toggle # hideBar / showBar +noct systemMonitor toggle +noct calendar toggle +noct state all # dump the shell's live state (JSON) +``` + +## msg — drive Niri from a shell + +```sh +niri msg outputs # monitors, modes, scale +niri msg workspaces # list, with the focused one marked +niri msg windows # every window: id, app-id, title, workspace +niri msg focused-window +niri msg pick-window # click a window, get its details (for window rules) +niri msg action <action> [args] # any bindable action, e.g. +niri msg action focus-workspace 3 +niri msg action set-column-width 50% +niri msg action switch-preset-column-width +niri msg action spawn -- kitty +niri msg -j windows | jq # JSON output for scripts +niri validate -c file.kdl # check a config file +``` + +## wallpaper — Rosé Pine + +Wallpapers are in `~/Pictures/Wallpapers/{rose-pine,rose-pine-dark,rose-pine-mid-walls}`. +Noctalia's picker (Mod+Ctrl+Space) and Caelestia's on Hyprland both read that folder and its subfolders +(`wallpaper.directory` in `noctalia.nix`, `wallpaperDir` in `caelestia.nix`). + +```sh +noct wallpaper toggle # the picker +noct wallpaper random "" # random from the configured folder +noct wallpaper set ~/Pictures/Wallpapers/rose-pine/koi-fish.png "" # any file ("" = all screens) +noct wallpaper get "" +``` + +## settings — making a change stick + +- **A Niri bind or option**: `modules/features/desktop/niri.nix` → `settings` (validated at build; a typo fails `nix build .#niri`). +- **A Noctalia setting**: change it in the GUI (lasts the session only; settings live in the store), + run `dump-noctalia-shell`, paste the differing keys into `noctalia.nix` → `settings`. +- Then `nh os switch`. A running Niri keeps its old config until re-login, or reload it now: + +```sh +niri msg action load-config-file --path "$(nix eval --raw ~/NixDaemon#niri)/niri-config.kdl" +``` + +## session — logging in and out + +```text +tuigreet: F2 → session list → "niri" (it remembers the last one) +Mod+Escape → Logout or Mod+Shift+E (niri asks to confirm) +back to Hyprland: log out, pick "Hyprland (UWSM)" +nix run ~/NixDaemon#niri nested test inside another desktop (Alt = Mod) +``` + +Caelestia never starts under Niri (its unit is tied to the Hyprland session). Idle: lock at 10 min, screen off at 20, never suspends. + +## fix — when something is off + +```sh +systemctl --user list-units --state=active --no-legend | grep -E 'caelestia|polkit|cliphist|udiskie' + # expect polkit, cliphist, udiskie — and NO caelestia +pkill -f noctalia; niri msg action spawn -- $(nix build --no-link --print-out-paths ~/NixDaemon#noctalia)/bin/noctalia-shell + # restart a crashed/hung bar +journalctl --user -b | grep -i niri # niri's log for this boot +Ctrl+Alt+F2 → log in → nh os rollback # can't log in at all +``` + +- "What's new" panel on first login: Noctalia's changelog, dismiss once and it's remembered (`~/.cache/noctalia/shell-state.json`). diff --git a/modules/home/cheats/nix.md b/modules/home/cheats/nix.md @@ -0,0 +1,266 @@ +# nix — rebuilding this machine from ~/NixDaemon + +NixOS **and** home-manager are one flake: `~/NixDaemon` (`NH_FLAKE` points at it). +home-manager runs as a NixOS module, so **one rebuild does both**; there is no separate `home-manager switch` here. +Terminal card: **`nix-cheat`** — sections `layout rebuild remote nh home search update rollback clean inspect add desktop dotfiles secrets repo shell`. + +## layout — what lives where + +Dendritic: `flake.nix` is `flake-parts.lib.mkFlake … (import-tree ./modules)`; every `*.nix` under `modules/` is a +flake-parts module that declares what it owns, and modules name each other through `self` (never by path). + +```text +~/NixDaemon/flake.nix inputs only · outputs = import-tree ./modules +modules/parts.nix systems, the home-manager + wrapper-modules flake modules +modules/hosts/laptop/default.nix flake.nixosConfigurations.nixos (modules = [ self.nixosModules.laptop ]) +modules/hosts/laptop/configuration.nix nixosModules.laptop: imports laptop-* + features by name; daemon.desktop.* switches +modules/hosts/laptop/*.nix nixosModules.laptop-<name>: hardware, nvidia, ssd, nix-settings (nh), sops, toolbox, fan-* +modules/features/workstation.nix nixosModules.workstation (Claude, Obsidian, gh, glab) +modules/features/home-manager.nix nixosModules.home-manager: HM as a NixOS module → self.homeModules.daemonsec +modules/features/desktop/options.nix daemon.desktop.hyprland.enable · daemon.desktop.niri.enable +modules/features/desktop/niri.nix packages.niri (wrapped) + nixosModules.desktop-niri +modules/features/desktop/noctalia.nix packages.noctalia (wrapped, Rosé Pine) +modules/home/default.nix homeModules.daemonsec: imports every homeModules.<name> +modules/home/tools.nix packages: the toolbox closure and the general CLI tools ← add packages here +modules/home/dotfiles.nix every dotfile from ~/git/daemon-sec-dotfiles, as symlinks ← add dotfile paths here +modules/home/hyprland.nix, hypr/*.lua Hyprland config + helpers (gated on daemon.desktop.hyprland) +modules/home/caelestia.nix the Caelestia shell, its CLI, wallpaper dir (same gate) +modules/hosts/laptop/sops.nix sops-nix (system) · modules/home/sops.nix (user) · secrets/secrets.yaml · .sops.yaml +``` + +## rebuild — nixos-rebuild, the plain way + +```sh +cd ~/NixDaemon +sudo nixos-rebuild switch --flake .#nixos # build, activate now, make it the boot default +sudo nixos-rebuild boot --flake .#nixos # build, activate on next boot only (kernel/driver changes) +sudo nixos-rebuild test --flake .#nixos # activate now, NOT the boot default (try something out) +nixos-rebuild build --flake .#nixos # just build → ./result, no sudo, nothing changes +sudo nixos-rebuild switch --flake ~/NixDaemon#nixos # same, from any directory +``` + +- `#nixos` is the configuration name (= hostname) and the only one in the flake (the old `#bootstrap` stage is gone). +- **New files must be known to git** or the flake does not see them: `git add path` first (see *repo*). +- A rebuild activates home-manager too; its log: `journalctl --user -u home-manager-daemonsec -b` or `systemctl status home-manager-daemonsec`. + +## remote — build straight from the GitLab repo + +The flake does not have to be checked out: any `nix` command takes a **flake reference** to the repo. +The repo is **private**, so use the ssh form (the key in ~/.ssh/config is registered on GitLab); `?ref=main` pins a branch, `?rev=<sha>` a commit. + +```sh +REPO='git+ssh://git@gitlab.com/DAEMON-404/NixDaemon.git' +sudo nixos-rebuild switch --flake "$REPO#nixos" # this machine, from the pushed main +sudo nixos-rebuild boot --flake "$REPO?ref=main#nixos" +nh os switch "$REPO" # nh takes the same reference +nix build "$REPO#nixosConfigurations.nixos.config.system.build.toplevel" --no-link --print-out-paths +nix flake show "$REPO" # what the repo exports +nix flake metadata "$REPO" # which commit nix resolved +# root (sudo) fetches with root's ssh: either run the fetch as you first (nix build …, cached), or give +# root the key via /root/.ssh/config, or use an https token in ~/.config/nix/netrc (machine gitlab.com …). +``` + +**Fresh machine, from the live ISO** (disk already partitioned and mounted at /mnt): +```sh +sudo nixos-generate-config --root /mnt --show-hardware-config > hardware-configuration.nix # compare with modules/hosts/laptop/hardware.nix +git clone git@gitlab.com:DAEMON-404/NixDaemon.git ~/NixDaemon && cd ~/NixDaemon # ssh key first (see *secrets*) +# paste its body into modules/hosts/laptop/hardware.nix (inside the laptop-hardware wrapper; a raw +# hardware-configuration.nix under modules/ would be loaded by import-tree as a flake-parts module), then: +sudo nixos-install --flake .#nixos +``` +Afterwards: clone `~/git/daemon-sec-dotfiles` (every dotfile links there), restore the age key to +`~/.config/sops/age/keys.txt` and `/var/lib/sops-nix/key.txt` (see *secrets*), and the toolbox to `~/.local/bin`. + +A remote reference is fetched fresh each time (nix caches it briefly, `--refresh` forces it); the local +checkout at `~/NixDaemon` (`NH_FLAKE`) is the default for day-to-day work. + +## nh — the same, with a diff and a progress tree + +`nh` wraps nixos-rebuild: builds with nix-output-monitor, shows an **nvd diff** of what changes, then asks for sudo. + +```sh +nh os switch # = sudo nixos-rebuild switch --flake ~/NixDaemon (NH_FLAKE), config picked by hostname +nh os boot # build + boot default, activate on reboot +nh os test # activate now, not the boot default +nh os build # build only, no activation, no sudo +nh os switch --dry # show what would happen, do nothing +nh os switch --ask # show the diff, then confirm before activating +nh os switch -H nixos # pick a configuration by name (-H = hostname/attr); this flake has one +nh os switch -u # `nix flake update` first, then switch (updates EVERY input — see *update*) +nh os info # list system generations +nh os rollback # go back one generation (see *rollback*) +nh os switch -- --show-trace # anything after -- goes to nix build (debug an eval error) +``` + +## home — home-manager in this setup + +- home-manager is **inside** the NixOS build (modules/features/home-manager.nix: user `daemonsec` → `self.homeModules.daemonsec`, i.e. modules/home/). + **`nh os switch` rebuilds it**; `nh home switch` / `home-manager switch` do not apply here (no standalone HM profile). +- Did my home change apply? `ls -l ~/.config/<thing>` (links point into /nix/store or the dotfiles checkout), + `systemctl --user status home-manager-daemonsec`, or `journalctl --user -u home-manager-daemonsec -b`. +- HM backs up a file it has to replace as `*.hm-bak` (`backupFileExtension` in modules/features/home-manager.nix). Delete the backup once happy. +- Only build the home part (fast check, no sudo): + `nix build ~/NixDaemon#nixosConfigurations.nixos.config.home-manager.users.daemonsec.home.activationPackage` + +## search — finding packages and options + +```sh +ns # fuzzy search everything indexed: nixpkgs packages, NixOS and home-manager options +ns kitty # start with a query +``` +Inside `ns` (fzf): type to filter, ↑↓ move, the right pane shows the description, version, homepage and +the option's default. **Enter** prints the attribute name (paste it into `home.packages` in tools.nix), +**ctrl-y** copies it, **ctrl-o** opens the homepage, **ctrl-s** the nixpkgs source, **Esc** quits. +The index is built by `nix-search-tv` on first use (a few seconds) and refreshed by itself; by hand: +```sh +nix-search-tv print | head # the raw list · nix-search-tv preview firefox # one entry +nh search firefox # search.nixos.org from the terminal (needs network) +nix search nixpkgs firefox # nix's own search (slow first run: builds an eval cache) +nix eval ~/NixDaemon#nixosConfigurations.nixos.pkgs.firefox.version # version THIS flake would install +nix shell nixpkgs#firefox # try it without installing · nix run nixpkgs#cowsay -- hi +``` + +## update — moving the inputs + +```sh +cd ~/NixDaemon +nix flake update # all inputs → flake.lock (nixpkgs, home-manager, hyprland, caelestia, llm-agents) +nix flake update nixpkgs home-manager # only these inputs +nix flake update hyprland # Hyprland alone (uses hyprland.cachix.org, so usually no compile) +nix flake lock # (re)write the lock without updating +nix flake metadata # which revisions are locked right now +nh os boot # then build it; boot = safest for kernel/driver bumps +``` + +Hyprland and Caelestia pin each other (flake.nix comments): update `hyprland` and `caelestia-*` together if the shell breaks. + +## rollback — when a generation misbehaves + +```sh +nh os rollback # previous generation, now +sudo nixos-rebuild switch --rollback # the same, plain +nh os info # generation numbers +sudo nix-env --switch-generation 5 -p /nix/var/nix/profiles/system && sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch +``` + +- At power-on the **systemd-boot menu** lists every generation: pick an older one, it boots as it was. +- A broken Hyprland login: Ctrl+Alt+F2 for a console, then one of the above. + +## clean — store and generations + +```sh +nh clean all --keep 5 --keep-since 14d # what the weekly timer runs (nix-settings.nix): drop old generations, GC +nh clean all --dry # show what it would remove +sudo nix-collect-garbage -d # the blunt version: delete all old generations, then GC +nix store gc # GC only (nothing referenced by a generation is touched) +du -sh /nix/store # how big is it +``` + +## inspect — see before you switch + +```sh +nh os build && nvd diff /run/current-system result # what a switch would change +nix build ~/NixDaemon#nixosConfigurations.nixos.config.system.build.toplevel --no-link --print-out-paths +nix eval ~/NixDaemon#nixosConfigurations.nixos.pkgs.uv.version # version a package would get +nix eval ~/NixDaemon#nixosConfigurations.nixos.config.programs.zsh.enable # read an option value +ns <query> # fuzzy search nixpkgs + NixOS/HM options, with descriptions (nix-search-tv) +nh search <package> # nixpkgs search (search.nixos.org) +nix search nixpkgs <package> # local search (first run builds an index) +nix shell nixpkgs#<package> # try a tool without installing it +nix run nixpkgs#<package> -- --help +nix flake check ~/NixDaemon # evaluate every output +nix flake show ~/NixDaemon +``` + +## add — packages, options, dotfiles, modules + +- **A package for the user**: `modules/home/tools.nix` → `home.packages` list → `nh os switch`. +- **A system package / service**: `modules/hosts/laptop/configuration.nix` (`environment.systemPackages`, `services.*`). +- **A dotfile from the checkout**: `modules/home/dotfiles.nix` → add its path to the list → `nh os switch`. +- **A new home module**: `modules/home/<name>.nix` declaring `flake.homeModules.<name> = { pkgs, ... }: { … };`, + then `<name>` in the imports list of `modules/home/default.nix`. A NixOS one: `flake.nixosModules.<name>` in + `modules/features/<name>.nix`, named in `configuration.nix`. `scripts/wrap.sh FILE ATTR` wraps a plain module file. +- **A Hyprland bind**: `modules/home/hypr/bindings.lua` (user) or `defaults.lua` (stock); a helper script goes in `hyprland.nix`. +- **A Niri bind**: `modules/features/desktop/niri.nix` → `settings.binds` (validated at build: a typo fails `nix build .#niri`). +- **A Caelestia setting**: `modules/home/caelestia.nix` → `programs.caelestia.settings`. +- **A Noctalia setting**: tweak it in the GUI, `dump-noctalia-shell`, paste the differing keys into `noctalia.nix` → `settings`. +- Then: `git add` anything new, `nh os switch`, and for Hyprland `hyprctl reload`. A running Niri keeps its old + store config until you log in again, or: `niri msg action load-config-file --path "$(nix eval --raw ~/NixDaemon#niri)/niri-config.kdl"`. + +## desktop — Hyprland + Caelestia or Niri + Noctalia + +Both are installed; tuigreet lists both sessions and remembers the last one, so switching is log out → pick the other. + +```sh +nix run ~/NixDaemon#niri # try Niri nested in the current desktop (Alt is the modifier); Noctalia starts inside it +nix run ~/NixDaemon#noctalia # the bar alone +``` +```nix +# modules/hosts/laptop/configuration.nix — set one to false and `nh os switch` to drop it entirely +daemon.desktop = { hyprland.enable = true; niri.enable = true; }; +``` + +## dotfiles — the checkout is the source of truth + +- `~/git/daemon-sec-dotfiles/home/` is mirrored into `$HOME` by **symlinks** (dotfiles.nix): edit a file there and the live config changes at once — no rebuild. +- A rebuild is needed only to add/remove a *path* in dotfiles.nix. `ls -l ~/.config/starship.toml` shows where a link goes. +- zsh: `~/.dotfiles/config/*.zsh` → open a new shell (`exec zsh`). Plugins are vendored in `~/.dotfiles/config/plugins`. +- Not linked on purpose (hypr, kitty, git, mimeapps, bash rc, .claude …): the list and reasons are at the top of dotfiles.nix. + +## secrets — sops-nix and secretspec + +Two tools, two jobs. **sops-nix**: secrets that belong in this repo, encrypted with **age**, decrypted at +activation to `/run/secrets/NAME` (system) or `~/.config/sops-nix/secrets/NAME` (user). +**secretspec**: runtime secrets for a *project*, declared in its `secretspec.toml`, stored in the keyring. + +```sh +# sops — the one key: ~/.config/sops/age/keys.txt (user) = /var/lib/sops-nix/key.txt (system, root copy) +sudo install -D -m 600 -o root -g root ~/.config/sops/age/keys.txt /var/lib/sops-nix/key.txt # once per machine +age-keygen -y ~/.config/sops/age/keys.txt # the public key (recipient) in .sops.yaml +sops secrets/secrets.yaml # edit: opens decrypted in $EDITOR, re-encrypts on save +sops set secrets/secrets.yaml '["wifi-psk"]' '"hunter2"' # add/replace one value without the editor +sops -d secrets/secrets.yaml # print decrypted +sops -d --extract '["example"]' secrets/secrets.yaml +sops updatekeys secrets/secrets.yaml # after editing the recipients in .sops.yaml +``` + +Then declare it and rebuild: +```nix +# modules/hosts/laptop/sops.nix (system) # modules/home/sops.nix (user) +sops.secrets.wifi-psk = { }; sops.secrets.my-token = { }; +sops.secrets."ssd.key".path = "/etc/secrets/ssd.key"; # e.g. the LUKS key instead of restoring it by hand +``` +`nh os switch` → `/run/secrets/wifi-psk` (root, 0400; `owner = "daemonsec";` to read it as the user). +Check: `sudo ls -l /run/secrets/`, `systemctl --user status sops-nix` (user side), `journalctl -b | grep sops`. + +```sh +# secretspec — in a project directory (config: ~/.config/secretspec/config.toml → keyring, profile default) +secretspec init # writes secretspec.toml (commit it; it holds names, never values) +secretspec add DATABASE_URL # declare a secret (flags: --required/--default/--description) +secretspec check # prompts for every missing value, stores it in the keyring +secretspec set NAME # (re)store one value +secretspec run -- ./server # run with the secrets in the environment +secretspec export # print them for another tool (shell `eval`) +secretspec claude configure # let Claude Code fetch its API credential through secretspec +``` + +## repo — committing ~/NixDaemon + +The repo is **jj, colocated with git** (`.jj` + `.git`). Nix flakes only see files git knows about, and jj keeps git's index at `@-`, so a *new* file is invisible to nix until it is added: + +```sh +cd ~/NixDaemon +git add modules/home/new.nix # make nix see a new file (modified tracked files are seen as-is) +nh os build # or switch +jj status # jj snapshots the working copy +jj describe -m "what changed" && jj new # seal it (the vault ritual) — or plain: git add -A && git commit +jj log # history +``` + +A dirty tree only prints `warning: Git tree '/home/daemonsec/NixDaemon' is dirty`; it still builds. + +## shell — after a switch + +- New packages are on PATH in a **new** shell (`exec zsh`); the login shell itself changes at the next login. +- Hyprland: `hyprctl reload` re-reads `~/.config/hypr/*.lua`. Caelestia: `caelestia shell -r` restarts the shell. +- Everything in the Nix profile: `ls /etc/profiles/per-user/daemonsec/bin`. diff --git a/modules/home/default.nix b/modules/home/default.nix @@ -0,0 +1,41 @@ +# modules/home/default.nix — the user's home-manager configuration: every +# home module in this directory, by name. Imported by the NixOS side in +# modules/features/home-manager.nix (home-manager runs as a NixOS module; +# `nh os switch` applies it, there is no standalone profile). +{ self, ... }: +{ + flake.homeModules.daemonsec = + { user, ... }: + { + imports = with self.homeModules; [ + hyprland # compositor config (Lua) and the carried shortcuts — only with daemon.desktop.hyprland + caelestia # programs.caelestia, shell.json, the Rosé Pine scheme — only with daemon.desktop.hyprland + session # polkit agent, cliphist, udiskie, screenshot and clipboard tools — for both desktops + terminal # kitty, fonts + tools # toolbox runtime closure, python env, dotfiles links + shell # zsh via the dotfiles ZDOTDIR tree, tmux plugins, secretspec + dotfiles # every dotfile from ~/git/daemon-sec-dotfiles, as out-of-store symlinks + cheats # nix-cheat: the rebuild / nh / flake card + sops # sops-nix for the user (same secrets file, age key in ~/.config/sops/age) + neovim # nvf: Neovim with a small, Nix-built plugin set + prompt # starship prompt and fastfetch card, Rosé Pine, NixOS logo + fan # `fan`: status/watch without root, max/auto with a clamp watchdog + ssh # the ssh key (sops) and ~/.ssh/config + gpg # the GPG main key (public in-repo, secret via sops) and gpg.conf + git # git identity, signing with the main key, delta + media # mpd + rmpc, mpv + yazi # yazi with previews, Rosé Pine, plugins + gtk # Yaru-purple icons, cursor, prefer-dark + ]; + + home = { + username = user; + homeDirectory = "/home/${user}"; + stateVersion = "26.05"; + }; + + programs.home-manager.enable = true; + xdg.enable = true; + } + ; +} diff --git a/modules/home/dotfiles.nix b/modules/home/dotfiles.nix @@ -0,0 +1,119 @@ +# modules/home/dotfiles.nix — every dotfile from the dotfiles checkout, placed +# by home-manager. +# +# ~/git/daemon-sec-dotfiles is the restorable snapshot of the Omarchy +# workstation (its README: "files restored relative to $HOME"). home-manager +# puts each of its files where it belongs as an out-of-store symlink +# (mkOutOfStoreSymlink), so there is one source of truth: edit the checkout and +# the live config changes; `nh os switch` is only needed when a path is added +# or removed here. The links dangle harmlessly until the repo is cloned. +# +# Not linked, and why (each is one line to add if wanted): +# .config/hypr, .config/kitty Nix-managed (modules/home/hyprland.nix, terminal.nix) +# .config/nvim the AstroNvim tree; Neovim is nvf now (modules/home/neovim.nix) +# .config/starship.toml, .config/fastfetch the Omarchy-era prompt and fetch; both are +# Nix-managed now (modules/home/prompt.nix) +# .config/mpd, rmpc, mpv, yazi Nix-managed (modules/home/media.nix, yazi.nix), carried from the checkout +# .config/omarchy*, Omacom, hyprmoncfg Omarchy's own trees; caelestia.nix reads the +# wallpaper directory straight from the checkout +# .config/systemd/user Omarchy-era units; caelestia-shell.service there would +# fight the home-manager caelestia service +# .config/autostart Omarchy autostarts for apps not installed here +# .config/mimeapps.list defaults point at chromium / HEY, neither installed: +# xdg-open would fail on every link +# .config/git turns on commit signing with a key not on this machine +# .config/fontconfig, dconf, gtk-4.0 home-manager writes these (fonts.fontconfig, gtk.nix) +# .config/gtk-3.0/bookmarks paths under the old /home/daemon-sec +# .config/user-dirs.dirs, floorp XDG defaults already match; a browser profile is state +# .config/tmux holds only a disabled backup; ~/.tmux.conf is the config +# .bashrc, .bash_profile, .bash_logout source Omarchy's bash rc unguarded (errors on NixOS) +# .zshrc, .zprofile the dead decoys; ZDOTDIR=~/.dotfiles bypasses them +# .XCompose includes /usr/share/omarchy/default/xcompose +# .claude, .codex, .agents live agent state on this machine (plugins, sessions) +# bin, .local/bin ~/.local/bin is the live toolbox repo (README) +# .local/share/applications Omarchy web-app launchers (omarchy-launch-webapp) +# .local/share/icons/hicolor apps install into it; the themes are linked one by one +{ ... }: +{ + flake.homeModules.dotfiles = + { config, lib, ... }: + let + repo = "${config.home.homeDirectory}/git/daemon-sec-dotfiles"; + home = "${repo}/home"; + link = path: config.lib.file.mkOutOfStoreSymlink "${home}/${path}"; + + # Same path under $HOME as in the checkout's home/. + same = paths: lib.genAttrs paths (p: { source = link p; }); + # Entries of .config, by name. + config' = names: lib.genAttrs names (n: { source = link ".config/${n}"; }); + + cursorThemes = [ + "modernxp-retro-black" # the active cursor (gtk.nix, core.lua) + "modernxp-retro-black-hyprcursor" + "modernxp-rose-pine" + "modernxp-rose-pine-hyprcursor" + "retrosmart-rose-pine" + "retrosmart-rose-pine-hyprcursor" + "rose-pine-hyprcursor" + "BreezeX-RosePine-Linux" + ]; + in + { + home.file = + same [ + ".dotfiles" # the zsh ZDOTDIR tree (modules/home/shell.nix sets ZDOTDIR) + ".tmux.conf" + ".ripgreprc" # RIPGREP_CONFIG_PATH, exported by .dotfiles/config/ripgrep.zsh + "Music/AGENTS.md" + ] + // same (map (t: ".local/share/icons/${t}") cursorThemes) + // { + # The patched DMMono TTFs live outside home/ in the checkout. + ".local/share/fonts/nerd-fonts-dm-mono".source = + config.lib.file.mkOutOfStoreSymlink "${repo}/assets/fonts/nerd-fonts-dm-mono"; + }; + + xdg.configFile = config' [ + # shell and prompt + "atuin" + "bat" # the "Rose Pine" theme BAT_THEME names (shell.nix rebuilds bat's cache) + "carapace" + "cheats" # the markdown cheat cards (cheats.zsh, ~/.local/bin/cheat-*) + "crossfetch" # the login splash animation (animations.zsh); the fetch card itself is prompt.nix + "eza" + "mise" + # tools + "btop" + "lazygit" + "jj" + "emacs" + "opencode" + "feroxbuster" + "uncover" + "herdr" + "tensaku" + "ai-usagebar" + "bg-pasticcio" + "libvirt" + # media + "cava" + "imv" + "zathura" + "xournalpp" + "spicetify" + "vesktop" + "pipewire" # 10-sample-rates.conf + "wireplumber" # bluetooth-a2dp-autoconnect.conf + # terminals and desktop bits + "alacritty" + "foot" + "ghostty" + "fcitx5" + "xdg-terminals.list" # kitty first, for xdg-terminal-exec + "chromium-flags.conf" # read only if a chromium is ever installed + "menus" # the chrome-apps application menu entries + "uwsm" # env.d/50-rose-pine-cursor (same values core.lua sets) + ]; + } + ; +} diff --git a/modules/home/fan.nix b/modules/home/fan.nix @@ -0,0 +1,125 @@ +# modules/home/fan.nix — `fan`: the laptop's fans from the terminal, safely. +# +# fan one line: CPU °C, fan rpm and %, clock cap, load, clamp yes/no, EC mode +# fan watch [s] the same line every s seconds (default 2), Ctrl-C to stop +# fan max 100 % now, with the watchdog (below) fan 60 fixed 60 % (30-100) +# fan auto back to the EC's own curve; stops the watchdog +# fan log what the watchdog has done +# +# Why a watchdog: this laptop's EC, once in manual fan mode, asserts PROCHOT +# and pins every core at 399 MHz as soon as load starts (fan-throttle-guard.nix). +# `fan max` therefore starts a transient user unit (fan-watchdog) that samples +# /proc/stat and /proc/cpuinfo every second and, the moment CPU busy ≥ 25 % +# while no core is above 600 MHz (fanfix's clamp rule), runs `fan-ec auto`, +# sends a notification and exits. Root access is `sudo -n fan-ec …` +# (modules/hosts/laptop/fan-cli.nix: a store script with a passwordless rule for wheel). +# Reads need no root at all: k10temp and the uniwill hwmon are world-readable. +{ ... }: +{ + flake.homeModules.fan = + { pkgs, lib, ... }: + let + bin = lib.makeBinPath [ pkgs.coreutils pkgs.gawk pkgs.gnugrep pkgs.gnused pkgs.systemd pkgs.libnotify ]; + # NixOS's setuid sudo lives in /run/wrappers; the store copy is not setuid + # and refuses to run ("must be owned by uid 0 and have the setuid bit set"). + sudo = "/run/wrappers/bin/sudo"; + + # shared read-only sampler, sourced by both scripts + lib-sh = pkgs.writeText "fan-lib.sh" '' + TRIP_MHZ=600; BUSY_MIN=25 + STATE=''${XDG_RUNTIME_DIR:-/tmp}/fan.stat + hw() { local n; for h in /sys/class/hwmon/hwmon*; do n=$(cat "$h/name" 2>/dev/null); [ "$n" = "$1" ] && { echo "$h"; return; }; done; } + cpu_temp() { local h; h=$(hw k10temp); [ -n "$h" ] && echo $(( $(cat "$h/temp1_input") / 1000 )) || echo -; } + fan_rpm() { local h; h=$(hw uniwill); [ -n "$h" ] && cat "$h/fan1_input" || echo -; } + fan_pct() { local h; h=$(hw uniwill); [ -n "$h" ] && echo $(( $(cat "$h/pwm1") * 100 / 255 )) || echo -; } + gpu_temp() { local h; h=$(hw uniwill); [ -n "$h" ] && echo $(( $(cat "$h/temp2_input") / 1000 )) || echo -; } + cap_mhz() { echo $(( $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq) / 1000 )); } + clocks() { awk '/^cpu MHz/ {s+=$4; n++; if ($4>m) m=$4} END {printf "%d %d", (n?s/n:0), m}' /proc/cpuinfo; } + # CPU busy % since the previous call (snapshot in $STATE); 0 on the first call + busy() { + local cur prev b=0 + cur=$(head -1 /proc/stat) + [ -r "$STATE" ] && prev=$(cat "$STATE") || prev= + printf '%s' "$cur" > "$STATE" + [ -n "$prev" ] && b=$(awk -v a="$prev" -v b="$cur" 'BEGIN { + na=split(a,x," "); nb=split(b,y," "); ta=0; tb=0 + for (i=2;i<=na;i++) ta+=x[i]; for (i=2;i<=nb;i++) tb+=y[i] + ia=x[5]+x[6]; ib=y[5]+y[6]; d=tb-ta; if (d<=0) {print 0; exit} + printf "%d", 100*(d-(ib-ia))/d }') + echo "$b" + } + clamped() { # 1 when busy yet no core above TRIP_MHZ + local b=$1 mx=$2; [ "$b" -ge "$BUSY_MIN" ] && [ "$mx" -gt 0 ] && [ "$mx" -lt "$TRIP_MHZ" ] && echo 1 || echo 0 + } + ec_mode() { ${sudo} -n /run/current-system/sw/bin/fan-ec mode 2>/dev/null || echo "?"; } + status_line() { + local b mx avg; b=$(busy); read -r avg mx <<< "$(clocks)" + local cl; cl=$(clamped "$b" "$mx") + printf 'CPU %s°C · GPU %s°C · fan %s rpm %s%% · cap %s MHz · clocks avg %s / max %s · load %s%% · clamp %s · fans %s\n' \ + "$(cpu_temp)" "$(gpu_temp)" "$(fan_rpm)" "$(fan_pct)" "$(cap_mhz)" "$avg" "$mx" "$b" \ + "$([ "$cl" = 1 ] && echo YES || echo no)" "$(ec_mode)" + } + ''; + + fan-watchdog = pkgs.writeShellScriptBin "fan-watchdog" '' + set -uo pipefail + PATH=${bin}:$PATH + . ${lib-sh} + LOG=''${XDG_STATE_HOME:-$HOME/.local/state}/fan-watchdog.log; mkdir -p "$(dirname "$LOG")" + log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$LOG"; } + log "armed: fans manual ($1), watching for the EC clamp" + busy >/dev/null; sleep 1 + while :; do + b=$(busy); read -r _ mx <<< "$(clocks)" + if [ "$(clamped "$b" "$mx")" = 1 ]; then + out=$(${sudo} -n /run/current-system/sw/bin/fan-ec auto 2>&1) + log "CLAMP: load $b %, max core $mx MHz -> fan-ec auto: $out" + notify-send -a fan -u critical "Fans back to auto" "EC clamp hit (load $b %, cores at $mx MHz). Manual fan mode released." + exit 0 + fi + sleep 1 + done + ''; + + fan = pkgs.writeShellScriptBin "fan" '' + set -uo pipefail + PATH=${bin}:$PATH + . ${lib-sh} + UNIT=fan-watchdog + EC=/run/current-system/sw/bin/fan-ec + LOG=''${XDG_STATE_HOME:-$HOME/.local/state}/fan-watchdog.log + + arm() { # start (or restart) the watchdog as a transient user unit + systemctl --user stop "$UNIT" 2>/dev/null || true + systemd-run --user --unit="$UNIT" --collect --quiet ${fan-watchdog}/bin/fan-watchdog "$1" \ + && echo "watchdog armed: back to auto the moment the EC clamps the CPU (fan log)" + } + disarm() { systemctl --user stop "$UNIT" 2>/dev/null || true; } + + if [ "$(id -u)" = 0 ]; then + echo "fan: run this as yourself, not under sudo (it needs your user session for the watchdog; root access is handled inside)" >&2 + exit 1 + fi + + case "''${1:-}" in + ""|status) status_line ;; + watch) + iv=''${2:-2}; busy >/dev/null; sleep "$iv" + while :; do status_line; sleep "$iv"; done ;; + max) ${sudo} -n "$EC" max && arm max ;; + auto) disarm; ${sudo} -n "$EC" auto ;; + [0-9]*) p=''${1%\%}; ${sudo} -n "$EC" "$p" && arm "$p %" ;; + ec) ${sudo} -n "$EC" status ;; + log) [ -r "$LOG" ] && tail -n ''${2:-20} "$LOG" || echo "no watchdog log yet" ;; + -h|--help|help) + echo "usage: fan [status] | watch [sec] | max | <30-100> | auto | ec | log [n]" + echo " max / <pct> put the fans in manual mode WITH the clamp watchdog; auto releases them." ;; + *) echo "fan: unknown command '$1' (try: fan help)" >&2; exit 2 ;; + esac + ''; + in + { + home.packages = [ fan fan-watchdog ]; + } + ; +} diff --git a/modules/home/git.nix b/modules/home/git.nix @@ -0,0 +1,86 @@ +# modules/home/git.nix — git, from the flake (was ~/.gitconfig written by the +# bootstrap script plus the dotfiles' .config/git, which is not linked). +# +# Identity: DAEMON-404 <zer0sec.xp@icloud.com>; every commit and tag signed +# with the GPG main key (modules/home/gpg.nix), which GitLab already knows. +# Credentials for https remotes come from gh / glab; clones use ssh anyway. +# delta paints diffs (Rosé Pine, from the dotfiles' git config). +{ ... }: +{ + flake.homeModules.git = + { ... }: + { + programs.git = { + enable = true; + signing = { + key = "3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4"; + format = "openpgp"; + signByDefault = true; # commit.gpgSign and tag.gpgSign + }; + settings = { + user = { + name = "DAEMON-404"; + email = "zer0sec.xp@icloud.com"; + }; + alias = { + co = "checkout"; + br = "branch"; + ci = "commit"; + st = "status"; + lg = "log --oneline --graph --decorate -20"; + }; + init.defaultBranch = "main"; + pull.rebase = true; + push.autoSetupRemote = true; + diff = { + algorithm = "histogram"; + colorMoved = "default"; + mnemonicPrefix = true; + }; + commit.verbose = true; + column.ui = "auto"; + branch.sort = "-committerdate"; + tag.sort = "-version:refname"; + rerere = { + enabled = true; + autoupdate = true; + }; + merge.conflictstyle = "zdiff3"; + core = { + autocrlf = "input"; + safecrlf = "warn"; + }; + credential = { + "https://github.com".helper = "!gh auth git-credential"; + "https://gist.github.com".helper = "!gh auth git-credential"; + "https://gitlab.com".helper = "!glab auth git-credential"; + }; + }; + }; + + programs.delta = { + enable = true; + enableGitIntegration = true; + options = { + navigate = true; + light = false; + line-numbers = true; + side-by-side = false; + hyperlinks = true; + syntax-theme = "none"; + minus-style = "\"#eb6f92\" \"#26233a\""; + minus-emph-style = "bold \"#eb6f92\" \"#403d52\""; + plus-style = "\"#9ccfd8\" \"#26233a\""; + plus-emph-style = "bold \"#31748f\" \"#403d52\""; + line-numbers-minus-style = "\"#eb6f92\""; + line-numbers-plus-style = "\"#31748f\""; + line-numbers-zero-style = "\"#6e6a86\""; + file-style = "\"#31748f\" bold"; + file-decoration-style = "\"#c4a7e7\" ul"; + hunk-header-style = "\"#eb6f92\" bold"; + hunk-header-decoration-style = "\"#6e6a86\" box"; + }; + }; + } + ; +} diff --git a/modules/home/gpg.nix b/modules/home/gpg.nix @@ -0,0 +1,56 @@ +# modules/home/gpg.nix — the GPG main key and gpg.conf, from the flake. +# +# public key modules/home/gpg/daemon-main.pub.asc → imported with ultimate trust (programs.gpg.publicKeys) +# secret key sops secret gpg_main_secret (the armored export, still under its own passphrase): +# imported into the keyring on activation if the keyring lacks it +# gpg.conf programs.gpg.settings (the gpg-cheat `setup` defaults) +# +# The agent itself is NixOS's (programs.gnupg.agent, pinentry-gnome3), so +# services.gpg-agent is deliberately not enabled here. +# Key: daemon (Main_Key) <zer0sec.xp@icloud.com>, RSA 4096, 2025-12-30, the one on GitLab. +{ ... }: +{ + flake.homeModules.gpg = + { config, pkgs, lib, ... }: + let + fpr = "3374F5B7E65B8CDB7949E2EBBFB092454FAFECB4"; + in + { + sops.secrets.gpg_main_secret = { }; + + programs.gpg = { + enable = true; + mutableKeys = true; # other people's keys and new subkeys stay editable + mutableTrust = true; + publicKeys = [ + { source = ./gpg/daemon-main.pub.asc; trust = 5; } + ]; + settings = { + default-key = fpr; + default-recipient-self = true; + keyid-format = "0xlong"; + with-fingerprint = true; + with-subkey-fingerprints = true; + personal-cipher-preferences = "AES256 AES192 AES"; + personal-digest-preferences = "SHA512 SHA384 SHA256"; + cert-digest-algo = "SHA512"; + no-emit-version = true; + no-comments = true; + keyserver = "hkps://keys.openpgp.org"; + auto-key-locate = "local,wkd"; + trust-model = "tofu+pgp"; + }; + }; + + # Import the secret key once (idempotent: skipped when the keyring has it). + # Runs after sops-nix has decrypted the secrets. + home.activation.gpgMainKey = lib.hm.dag.entryAfter [ "sops-nix" ] '' + if [ -r "${config.sops.secrets.gpg_main_secret.path}" ] \ + && ! ${pkgs.gnupg}/bin/gpg --batch --list-secret-keys ${fpr} >/dev/null 2>&1; then + run ${pkgs.gnupg}/bin/gpg --batch --quiet --import "${config.sops.secrets.gpg_main_secret.path}" \ + && echo "gpg: imported the main secret key ${fpr}" + fi + ''; + } + ; +} diff --git a/home/gpg/daemon-main.pub.asc b/modules/home/gpg/daemon-main.pub.asc diff --git a/modules/home/gtk.nix b/modules/home/gtk.nix @@ -0,0 +1,31 @@ +# modules/home/gtk.nix — icons, cursor, dark preference. +# The cursor theme files are symlinked from the dotfiles checkout in tools.nix +# (modernxp-retro-black for Xcursor, modernxp-retro-black-hyprcursor for +# Hyprland), so there is no package to point home.pointerCursor at. +{ ... }: +{ + flake.homeModules.gtk = + { pkgs, ... }: + { + gtk = { + enable = true; + iconTheme = { + name = "Yaru-purple"; + package = pkgs.yaru-theme; + }; + cursorTheme = { + name = "modernxp-retro-black"; + size = 24; + }; + colorScheme = "dark"; # GTK prefer-dark + }; + + home.sessionVariables = { + XCURSOR_THEME = "modernxp-retro-black"; + XCURSOR_SIZE = "24"; + HYPRCURSOR_THEME = "modernxp-retro-black-hyprcursor"; # name from the theme's manifest.hl + HYPRCURSOR_SIZE = "24"; + }; + } + ; +} diff --git a/home/hypr/bindings.lua b/modules/home/hypr/bindings.lua diff --git a/home/hypr/caelestia.lua b/modules/home/hypr/caelestia.lua diff --git a/home/hypr/core.lua b/modules/home/hypr/core.lua diff --git a/home/hypr/defaults.lua b/modules/home/hypr/defaults.lua diff --git a/home/hypr/lid.lua b/modules/home/hypr/lid.lua diff --git a/home/hypr/looknfeel.lua b/modules/home/hypr/looknfeel.lua diff --git a/home/hypr/omarchy.lua b/modules/home/hypr/omarchy.lua diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix @@ -0,0 +1,134 @@ +# modules/home/hyprland.nix — Hyprland (Lua config) and the carried shortcuts. +# +# Hyprland 0.56 is configured in Lua (hyprland.lua, `hl.*` API); that is also +# the dialect the carried shortcuts and the toolbox helpers (dropterm, winsnap, +# vault-open, lid-control: `hyprctl dispatch 'hl.dsp…'`) already speak. The +# config is split like the vault's shortcuts/: +# hypr/omarchy.lua the `o` helpers the carried files were written against +# hypr/core.lua monitor, env, look, input, Caelestia layer rules +# hypr/looknfeel.lua the springy animations, shadows, snap, swallow (dotfiles looknfeel.lua) +# hypr/defaults.lua the stock Omarchy binds as captured in keybinds.txt +# hypr/bindings.lua shortcuts/bindings.lua (user overrides, window mode) +# hypr/lid.lua shortcuts/lid.lua +# hypr/caelestia.lua shortcuts/caelestia.lua (Caelestia keys, always on here) +{ ... }: +{ + flake.homeModules.hyprland = + { config, pkgs, lib, inputs, osConfig, ... }: + let + system = pkgs.stdenv.hostPlatform.system; + hyprPkg = inputs.hyprland.packages.${system}.hyprland; + shellCli = "${config.programs.caelestia.cli.package}/bin/caelestia"; + + # Small helpers the stock Omarchy binds relied on. They exist only to serve + # this config, so they live here rather than in ~/.local/bin. + helpers = [ + # The picker the omarchy-menu-* cheat sheets (bin/) pipe into. Prints the chosen line. + (pkgs.writeShellScriptBin "omarchy-menu-select" '' + title=''${1:-Select} + exec ${pkgs.fuzzel}/bin/fuzzel --dmenu --prompt "$title › " --width 110 --lines 24 + '') + # omarchy-not-ported "<bind description>" ["<what it did on Omarchy>"]: an + # honest notification instead of a key that silently does nothing. + (pkgs.writeShellScriptBin "omarchy-not-ported" '' + ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 4000 "$1" "Not set up on this NixOS build.''${2:+ Omarchy: $2}" + '') + (pkgs.writeShellScriptBin "nixdaemon-show" '' + # nixdaemon-show time|battery|calendar|kbd-backlight-cycle + n() { ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 5000 "$@"; } + case "''${1:-}" in + time) n "$(date '+%H:%M')" "$(date '+%A %-d %B %Y')" ;; + battery) b=/sys/class/power_supply/BAT0; n "Battery $(cat $b/capacity)%" "$(cat $b/status)" ;; + calendar) n "$(date '+%B %Y')" "$(cal | tail -n +2)" ;; + kbd-backlight-cycle) + d=$(ls -d /sys/class/leds/*kbd_backlight 2>/dev/null | head -1); [ -n "$d" ] || exit 0 + max=$(cat "$d/max_brightness"); cur=$(cat "$d/brightness"); next=$(( (cur + 1) % (max + 1) )) + ${pkgs.brightnessctl}/bin/brightnessctl -q -d "$(basename "$d")" set "$next" ;; + *) echo "usage: nixdaemon-show time|battery|calendar|kbd-backlight-cycle" >&2; exit 2 ;; + esac + '') + # SUPER+K: searchable list of the live binds (what Omarchy's keybindings menu did). Enter copies the key. + (pkgs.writeShellScriptBin "keybinds-menu" '' + sel=$(hyprctl binds -j | ${pkgs.python3}/bin/python3 -I -c ' + import json, sys + M = {1: "SHIFT", 4: "CTRL", 8: "ALT", 64: "SUPER"} + rows = set() + for x in json.load(sys.stdin): + mods = "+".join(n for v, n in sorted(M.items()) if x["modmask"] & v) + key = x["key"] or ("code:%d" % x["keycode"]) + sub = x.get("submap", "") + rows.add(("%s%s%s %s" % (sub + ": " if sub else "", mods + " + " if mods else "", key, x.get("description", ""))).rstrip()) + print("\n".join(sorted(rows)))' | ${pkgs.fuzzel}/bin/fuzzel --dmenu --prompt "Keybindings › " --width 120 --lines 30) || exit 0 + [ -n "$sel" ] && printf '%s' "''${sel%% *}" | ${pkgs.wl-clipboard}/bin/wl-copy + '') + # CTRL+SUPER+SPACE: what Omarchy's background switcher did, with Caelestia's + # own wallpaper grid. The launcher shows it when its search starts with + # ">wallpaper ", and there is no IPC for that, so the prefix is typed in. + (pkgs.writeShellScriptBin "wallpaper-picker" '' + c=${shellCli} + case "$($c shell drawers isOpen launcher 2>/dev/null)" in + 1|true) exec $c shell drawers toggle launcher ;; + esac + $c shell drawers toggle launcher + sleep 0.25 + exec ${pkgs.wtype}/bin/wtype '>wallpaper ' + '') + (pkgs.writeShellScriptBin "nightlight-toggle" '' + if pgrep -x hyprsunset >/dev/null; then + pkill -x hyprsunset; ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 2000 "Night light" "off" + else + ${pkgs.hyprsunset}/bin/hyprsunset --temperature 4000 >/dev/null 2>&1 & + ${pkgs.libnotify}/bin/notify-send -a NixDaemon -t 2000 "Night light" "on (4000 K)" + fi + '') + ]; + in + # Only when the Hyprland desktop is switched on (modules/features/desktop/options.nix). + lib.mkIf osConfig.daemon.desktop.hyprland.enable { + wayland.windowManager.hyprland = { + enable = true; + package = hyprPkg; # same derivation NixOS installs; no second copy + portalPackage = null; # programs.hyprland (NixOS) provides the portal + configType = "lua"; + systemd.enable = false; # uwsm owns graphical-session.target + xwayland.enable = true; + + extraLuaFiles = { + omarchy = { content = ./hypr/omarchy.lua; autoLoad = false; }; + core = { content = ./hypr/core.lua; autoLoad = false; }; + looknfeel = { content = ./hypr/looknfeel.lua; autoLoad = false; }; + defaults = { content = ./hypr/defaults.lua; autoLoad = false; }; + bindings = { content = ./hypr/bindings.lua; autoLoad = false; }; + lid = { content = ./hypr/lid.lua; autoLoad = false; }; + caelestia = { content = ./hypr/caelestia.lua; autoLoad = false; }; + }; + + # Explicit load order: look first, then the stock binds, then the carried files that + # unbind-and-rebind the keys they take over, Caelestia's keys last. + extraConfig = '' + local cfg = (os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME") .. "/.config")) .. "/hypr" + package.path = cfg .. "/?.lua;" .. package.path + require("omarchy") + require("core") + require("looknfeel") + require("defaults") + require("bindings") + require("lid") + require("caelestia") + ''; + }; + + home.packages = helpers ++ (with pkgs; [ + hyprpicker # colour picker (SUPER+PRINT) + hyprsunset # night light + fuzzel # dmenu for the cheat sheets and keybinds-menu + nautilus # the file manager shell.json and the stock binds point at + brightnessctl + pamixer + ]); # grim, slurp, wl-clipboard, libnotify: session.nix (shared with Niri) + + # The polkit agent, cliphist and udiskie (graphical-session.target) are in + # session.nix: both desktops need them. + } + ; +} diff --git a/home/kitty/scrollback.lua b/modules/home/kitty/scrollback.lua diff --git a/modules/home/media.nix b/modules/home/media.nix @@ -0,0 +1,120 @@ +# modules/home/media.nix — music and video: mpd + rmpc, and mpv. +# +# mpd runs as a user service (starts at login), library ~/Music (the layout +# contract is ~/Music/AGENTS.md), state in ~/.local/share/mpd, PipeWire +# output plus the FIFO rmpc's visualiser reads. It listens on the socket +# $XDG_RUNTIME_DIR/mpd/socket and on 127.0.0.1:6600. +# +# rmpc: the dotfiles' full config (modules/home/rmpc/config.ron: tabs with album art, +# lyrics and cava panes, vim keys, 1-0 tab switching), the Rosé Pine theme +# (modules/home/rmpc/themes/rose-pine.ron) and the LRCLIB lyrics fetcher that runs on +# song change. The Discord presence script was not carried (1.4k lines of +# Python with its own deps; say so if wanted). +# +# mpv: the dotfiles' gpu-next/Vulkan profile with the CfL chroma shader, plus +# uosc (the on-screen UI), thumbfast (seek thumbnails) and mpris (media keys, +# Caelestia's player widget). +{ ... }: +{ + flake.homeModules.media = + { config, pkgs, ... }: + let + rt = "/run/user/1000"; + in + { + services.mpd = { + enable = true; + musicDirectory = "${config.home.homeDirectory}/Music"; + playlistDirectory = "${config.xdg.dataHome}/mpd/playlists"; + network = { + listenAddress = "${rt}/mpd/socket"; + port = 6600; + startWhenNeeded = false; + }; + extraConfig = '' + bind_to_address "127.0.0.1" + auto_update "yes" + restore_paused "yes" + audio_output { + type "pipewire" + name "PipeWire" + } + audio_output { + type "fifo" + name "Visualizer FIFO" + path "${rt}/mpd/fifo" + format "44100:16:2" + } + ''; + }; + # the socket's directory, created by systemd before mpd starts + systemd.user.services.mpd.Service.RuntimeDirectory = "mpd"; + + programs.rmpc = { + enable = true; + config = builtins.readFile ./rmpc/config.ron; + }; + xdg.configFile = { + "rmpc/themes/rose-pine.ron".source = ./rmpc/themes/rose-pine.ron; + "rmpc/scripts/fetch-lyrics" = { + source = ./rmpc/scripts/fetch-lyrics; + executable = true; + }; + "mpv/shaders".source = ./mpv/shaders; + }; + + programs.mpv = { + enable = true; + scripts = with pkgs.mpvScripts; [ uosc thumbfast mpris ]; + config = { + vo = "gpu-next"; + gpu-api = "vulkan"; + gpu-context = "waylandvk"; + profile = "high-quality"; + hwdec = "auto"; + scale = "ewa_lanczos4sharpest"; + glsl-shader = "~~/shaders/CfL_Prediction.glsl"; + cscale = "ewa_lanczossharp"; + cscale-antiring = 0.65; + dscale = "mitchell"; + correct-downscaling = true; + linear-downscaling = true; + sigmoid-upscaling = true; + deband = true; + deband-iterations = 2; + deband-threshold = 32; + deband-range = 12; + deband-grain = 16; + dither = "error-diffusion"; + error-diffusion = "burkes"; + dither-depth = 8; + temporal-dither = false; + video-sync = "display-resample"; + interpolation = true; + tscale = "oversample"; + target-colorspace-hint = "auto"; + target-colorspace-hint-mode = "target"; + target-prim = "bt.709"; + target-trc = "srgb"; + gamut-mapping-mode = "perceptual"; + tone-mapping = "auto"; + hdr-compute-peak = true; + contrast = 4; + saturation = 5; + screenshot-format = "png"; + screenshot-high-bit-depth = true; + screenshot-tag-colorspace = true; + screenshot-directory = "~/Pictures/mpv"; + osc = false; # uosc replaces it + border = false; + save-position-on-quit = true; + keep-open = true; + sub-auto = "fuzzy"; + slang = "en,eng"; + alang = "ja,jpn,en,eng"; + ytdl-format = "bestvideo[height<=?1440]+bestaudio/best"; + }; + }; + } + ; +} diff --git a/home/mpv/shaders/CfL_Prediction.LICENSE b/modules/home/mpv/shaders/CfL_Prediction.LICENSE diff --git a/home/mpv/shaders/CfL_Prediction.glsl b/modules/home/mpv/shaders/CfL_Prediction.glsl diff --git a/modules/home/neovim.nix b/modules/home/neovim.nix @@ -0,0 +1,120 @@ +# modules/home/neovim.nix — Neovim through nvf (github:notashelf/nvf): the +# editor and its plugins are one Nix-built package, no plugin manager, no +# ~/.config/nvim, nothing downloaded on first start. Replaces the AstroNvim +# tree the dotfiles carried (2026-10-08): that one pulled ~60 plugins through +# lazy.nvim and compiled treesitter parsers on the machine. +# +# Kept deliberately small. Languages: the ones this machine edits (Nix, Lua +# for Hyprland, Python and Bash for the toolbox, Markdown for the vault, and +# the config formats). Each gets treesitter, an LSP and a formatter; format on +# save is off, `<leader>lf` formats on demand. +# +# <leader>ff / fg / fb telescope: files / live grep / buffers +# - oil: edit the parent directory as a buffer +# <leader>e oil in a floating window +# gd gr K <leader>ca LSP: definition, references, hover, code action (nvf defaults) +# <leader>lf format buffer +# ]c [c <leader>gp gitsigns: next/previous hunk, preview hunk +# gcc gc{motion} comment.nvim +# <Esc> clear search highlight +# <space> leader +# +# kitty's copy mode (modules/home/terminal.nix) starts plain pkgs.neovim with +# -u, so it is unaffected by this configuration and stays instant. +{ ... }: +{ + flake.homeModules.neovim = + { inputs, pkgs, ... }: + { + imports = [ inputs.nvf.homeManagerModules.default ]; + + programs.nvf = { + enable = true; + settings.vim = { + viAlias = true; + vimAlias = true; + + theme = { + enable = true; + name = "rose-pine"; + style = "main"; # main, not moon + transparent = false; + }; + + # Editor behaviour + lineNumberMode = "relNumber"; + searchCase = "smart"; + preventJunkFiles = true; + undoFile.enable = true; + clipboard = { + enable = true; + registers = "unnamedplus"; + providers.wl-copy.enable = true; + }; + options = { + tabstop = 2; + shiftwidth = 2; + softtabstop = 2; + scrolloff = 6; + wrap = false; + signcolumn = "yes"; + cursorline = true; + splitbelow = true; + splitright = true; + updatetime = 250; + timeoutlen = 400; + }; + + # Languages: treesitter + LSP + formatter each, chosen by nvf's defaults + # (nil for Nix, basedpyright/ruff for Python, lua-language-server, + # bash-language-server/shfmt, marksman, yaml/json/taplo). + lsp = { + enable = true; + formatOnSave = false; + inlayHints.enable = false; + }; + languages = { + enableTreesitter = true; + enableFormat = true; + nix = { + enable = true; + format.type = [ "nixfmt" ]; # the style this repo is written in + }; + lua.enable = true; + python.enable = true; + bash.enable = true; + markdown.enable = true; + yaml.enable = true; + json.enable = true; + toml.enable = true; + }; + + autocomplete.blink-cmp.enable = true; + telescope.enable = true; + git.gitsigns.enable = true; + binds.whichKey.enable = true; + statusline.lualine.enable = true; + autopairs.nvim-autopairs.enable = true; + comments.comment-nvim.enable = true; + utility.oil-nvim.enable = true; + visuals.nvim-web-devicons.enable = true; + ui.borders.enable = true; + + keymaps = [ + { key = "-"; mode = "n"; action = "<cmd>Oil<CR>"; desc = "Open parent directory"; silent = true; } + { key = "<leader>e"; mode = "n"; action = "<cmd>Oil --float<CR>"; desc = "Explorer (oil)"; silent = true; } + { key = "<Esc>"; mode = "n"; action = "<cmd>nohlsearch<CR>"; desc = "Clear search highlight"; silent = true; } + { key = "<leader>w"; mode = "n"; action = "<cmd>write<CR>"; desc = "Save"; silent = true; } + { key = "<leader>q"; mode = "n"; action = "<cmd>quit<CR>"; desc = "Quit"; silent = true; } + { key = "<C-h>"; mode = "n"; action = "<C-w>h"; desc = "Window left"; } + { key = "<C-j>"; mode = "n"; action = "<C-w>j"; desc = "Window down"; } + { key = "<C-k>"; mode = "n"; action = "<C-w>k"; desc = "Window up"; } + { key = "<C-l>"; mode = "n"; action = "<C-w>l"; desc = "Window right"; } + { key = "<"; mode = "v"; action = "<gv"; desc = "Dedent, keep selection"; } + { key = ">"; mode = "v"; action = ">gv"; desc = "Indent, keep selection"; } + ]; + }; + }; + } + ; +} diff --git a/modules/home/prompt.nix b/modules/home/prompt.nix @@ -0,0 +1,250 @@ +# modules/home/prompt.nix — the starship prompt and the fastfetch card, fresh +# (2026-10-08), Rosé Pine, one colour per section, nothing Omarchy. +# +# Prompt (two lines, the dotfiles' "filigree" frame kept, the per-letter +# gradients gone): +# +# ╭╌ ☧ daemonsec@nixos ┄ 󰉋 ~/NixDaemon ┄ main [+2 !1] ⌁⡇⡆· (right: 󰔚 3s · 󰥔 14:02) +# ╰╌ ❯ +# +# glyph iris · user rose · host foam · directory gold · git love (status subtle, +# week heartbeat iris) · languages text · duration/jobs gold · clock rose · +# prompt char foam (love after an error). pine is never ink (3.3:1 on base). +# $CROSS_GLYPH comes from the dotfiles' animations.zsh (☧ + the NixOS glyph). +# +# theme.zsh in the dotfiles runs `starship init zsh` and adds the transient +# prompt, so home-manager's own shell integration stays off here. +# +# fastfetch: the builtin NixOS logo in iris/foam, keys in rotating Rosé Pine +# colours, the modules that matter on this laptop. The login splash +# (animations.zsh) runs plain `fastfetch` when CROSS_FETCH=plain, which +# .dotfiles/.zshrc now sets, so this config draws the whole card. +{ ... }: +{ + flake.homeModules.prompt = + { lib, ... }: + let + # Rosé Pine (main) + rp = { + love = "#eb6f92"; + gold = "#f6c177"; + rose = "#ebbcba"; + pine = "#31748f"; + foam = "#9ccfd8"; + iris = "#c4a7e7"; + text = "#e0def4"; + subtle = "#908caa"; + muted = "#6e6a86"; + }; + # the same colours as SGR parameters for fastfetch + sgr = { + love = "38;2;235;111;146"; + gold = "38;2;246;193;119"; + rose = "38;2;235;188;186"; + foam = "38;2;156;207;216"; + iris = "38;2;196;167;231"; + text = "38;2;224;222;244"; + subtle = "38;2;144;140;170"; + muted = "38;2;110;106;134"; + }; + lang = symbol: colour: { + inherit symbol; + format = " [$symbol($version)](fg:${colour})"; + }; + in + { + programs.starship = { + enable = true; + enableZshIntegration = false; # theme.zsh does it (with the transient prompt) + enableBashIntegration = false; + settings = { + "$schema" = "https://starship.rs/config-schema.json"; + add_newline = true; + palette = "rose_pine"; + palettes.rose_pine = rp; + + format = lib.concatStrings [ + "[╭╌](fg:muted) " + "\${env_var.CROSS_GLYPH}" + "$username" + "$hostname" + "$shlvl" + "$sudo" + "\${custom.root}" + "$directory" + "$git_branch" + "$git_status" + "\${custom.gitweek}" + "$git_state" + "$python" + "$nodejs" + "$rust" + "$golang" + "$lua" + "$docker_context" + "$package" + "$line_break" + "[╰╌](fg:muted) " + "$status" + "$character" + ]; + right_format = lib.concatStrings [ "$cmd_duration" "$jobs" "$battery" "$time" ]; + + # identity + env_var.CROSS_GLYPH = { + variable = "CROSS_GLYPH"; + default = "☧"; + format = "[$env_value](bold fg:iris) "; + }; + username = { + show_always = true; + format = "[$user](bold fg:rose)"; + style_user = "bold fg:rose"; + style_root = "bold fg:love"; + }; + hostname = { + ssh_only = false; + format = "[@](fg:muted)[$hostname](bold fg:foam)"; + }; + shlvl = { + disabled = false; + threshold = 2; + format = " [↕$shlvl](bold fg:gold)"; + }; + sudo = { + disabled = false; + format = " [](bold fg:love)"; + }; + custom.root = { + command = "echo ROOT"; + when = "[ \"$(id -u)\" -eq 0 ]"; + format = " [ $output](bold underline fg:love)"; + }; + + # place + directory = { + format = " [┄](fg:muted) [󰉋 $path](bold fg:gold)[$read_only](fg:love)"; + truncation_length = 4; + truncate_to_repo = true; + truncation_symbol = "…/"; + read_only = " 󰌾"; + }; + + # git + git_branch = { + symbol = " "; + format = " [┄](fg:muted) [$symbol$branch(:$remote_branch)](bold fg:love)"; + }; + git_status = { + format = "( [\\[$all_status$ahead_behind\\]](fg:subtle))"; + ahead = "⇡\${count}"; + behind = "⇣\${count}"; + diverged = "⇕⇡\${ahead_count}⇣\${behind_count}"; + conflicted = "="; + untracked = "?"; + stashed = "≡"; + modified = "!"; + staged = "+"; + renamed = "»"; + deleted = "✘"; + }; + # the last 7 days of commits as a braille pulse (carried from the dotfiles) + custom.gitweek = { + command = ''git log --since=7.days --date=format:%Y%m%d --pretty=%cd 2>/dev/null | sort | uniq -c | awk 'BEGIN{split("· ⡀ ⡄ ⡆ ⡇",b," ")}{c[$2]=$1}END{for(i=6;i>=0;i--){d=strftime("%Y%m%d",systime()-i*86400);v=c[d]+0;idx=(v==0)?1:(v<3)?2:(v<6)?3:(v<10)?4:5;printf "%s",b[idx]}}' ''; + when = "git rev-parse --is-inside-work-tree 2>/dev/null | grep -q true"; + format = " [⌁$output](fg:iris)"; + }; + git_state = { + format = " [\\($state $progress_current/$progress_total\\)](bold fg:love)"; + }; + + # toolchains: only when the directory uses them + python = (lang " " "text") // { format = " [$symbol$version( \\($virtualenv\\))](fg:text)"; }; + nodejs = lang " " "text"; + rust = lang " " "text"; + golang = lang " " "text"; + lua = lang " " "text"; + docker_context = { symbol = " "; format = " [$symbol$context](fg:subtle)"; }; + package = { symbol = "󰏗 "; format = " [$symbol$version](fg:subtle)"; }; + + # right side + cmd_duration = { min_time = 2000; format = "[󰔚 $duration](fg:gold) "; }; + jobs = { symbol = "󰜎 "; format = "[$symbol$number](bold fg:gold) "; }; + battery = { + full_symbol = "󱈑 "; + charging_symbol = "󰂄 "; + discharging_symbol = "󱈏 "; + unknown_symbol = "󰂑 "; + empty_symbol = "󰁺 "; + format = "[$symbol$percentage]($style) "; + display = [ + { threshold = 20; style = "bold fg:love"; } + { threshold = 50; style = "fg:gold"; } + ]; + }; + time = { + disabled = false; + time_format = "%H:%M"; + format = "[󰥔 $time](fg:rose)"; + }; + + # second line + status = { + disabled = false; + symbol = "✗ "; + format = "[$symbol$status](fg:love) "; + }; + character = { + success_symbol = "[❯](bold fg:foam)"; + error_symbol = "[❯](bold fg:love)"; + vimcmd_symbol = "[❮](bold fg:gold)"; + vimcmd_replace_one_symbol = "[❮](bold fg:rose)"; + vimcmd_replace_symbol = "[❮](bold fg:iris)"; + vimcmd_visual_symbol = "[❮](bold fg:gold)"; + }; + line_break.disabled = false; + }; + }; + + programs.fastfetch = { + enable = true; + settings = { + "$schema" = "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json"; + logo = { + type = "builtin"; + source = "nixos"; + color = { "1" = sgr.iris; "2" = sgr.foam; }; + padding = { top = 1; left = 2; right = 5; }; + }; + display = { + separator = " "; + color = { keys = sgr.foam; title = sgr.rose; }; + }; + modules = [ + { type = "title"; color = { user = sgr.rose; at = sgr.muted; host = sgr.foam; }; } + { type = "separator"; string = "┄"; length = 34; outputColor = sgr.muted; } + { type = "os"; key = " os"; keyColor = sgr.iris; } + { type = "kernel"; key = " kernel"; keyColor = sgr.foam; } + { type = "uptime"; key = " uptime"; keyColor = sgr.gold; } + { type = "packages"; key = "󰏗 packages"; keyColor = sgr.rose; } + { type = "shell"; key = " shell"; keyColor = sgr.love; } + "break" + { type = "wm"; key = " wm"; keyColor = sgr.iris; } + { type = "display"; key = "󱄄 display"; keyColor = sgr.foam; compactType = "original-with-refresh-rate"; } + { type = "terminal"; key = " terminal"; keyColor = sgr.gold; } + { type = "terminalfont"; key = " font"; keyColor = sgr.rose; } + "break" + { type = "host"; key = "󰌢 host"; keyColor = sgr.love; } + { type = "cpu"; key = " cpu"; keyColor = sgr.iris; showPeCoreCount = true; } + { type = "gpu"; key = " gpu"; keyColor = sgr.foam; detectionMethod = "pci"; format = "{name}"; } + { type = "memory"; key = " memory"; keyColor = sgr.gold; } + { type = "disk"; key = "󰋊 disk"; keyColor = sgr.rose; folders = "/"; } + { type = "battery"; key = "󰁹 battery"; keyColor = sgr.love; } + "break" + { type = "colors"; symbol = "circle"; paddingLeft = 2; } + ]; + }; + }; + } + ; +} diff --git a/home/rmpc/config.ron b/modules/home/rmpc/config.ron diff --git a/home/rmpc/scripts/fetch-lyrics b/modules/home/rmpc/scripts/fetch-lyrics diff --git a/home/rmpc/themes/rose-pine.ron b/modules/home/rmpc/themes/rose-pine.ron diff --git a/modules/home/session.nix b/modules/home/session.nix @@ -0,0 +1,39 @@ +# modules/home/session.nix — what every Wayland session needs, whichever +# compositor is running: the polkit agent (privilege prompts), clipboard +# history, auto-mounting, and the screenshot / clipboard tools the Niri binds +# and the toolbox scripts expect on PATH. Not gated on a desktop switch, so a +# Niri-only system keeps working. Everything hangs off graphical-session.target, +# which uwsm (Hyprland) and niri-session both manage. +{ ... }: +{ + flake.homeModules.session = + { pkgs, ... }: + { + home.packages = with pkgs; [ + grim + slurp + wl-clipboard + libnotify + ]; + + systemd.user.services.hyprpolkitagent = { + Unit = { + Description = "Hyprland polkit authentication agent"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = "${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent"; + Restart = "on-failure"; + Slice = "session.slice"; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; + services.cliphist.enable = true; # history for `caelestia clipboard` and Noctalia's clipboard tab + services.udiskie = { + enable = true; + tray = "auto"; + }; + } + ; +} diff --git a/modules/home/shell.nix b/modules/home/shell.nix @@ -0,0 +1,120 @@ +# modules/home/shell.nix — zsh as the shell, configured by the dotfiles. +# +# The dotfiles checkout (~/git/daemon-sec-dotfiles) carries a complete zsh +# setup in home/.dotfiles: a ZDOTDIR tree with core.zsh (history, cached +# compinit, the plugin loader, zoxide/atuin/fzf hooks), the leaf modules +# (aliases, fzf verbs, ripgrep, cheats, music, jj, …), theme.zsh (starship +# with a transient prompt) and animations.zsh (the login splash). The plugins +# it loads — zsh-autosuggestions, zsh-syntax-highlighting, zsh-completions, +# fzf-tab, history-substring-search, you-should-use — are vendored in +# .dotfiles/config/plugins, so the config is used as-is rather than rewritten +# as home-manager options. This module only supplies what the Omarchy install +# had and NixOS does not: +# +# ~/.zshenv sets ZDOTDIR (home-manager owns this one file) +# ~/.dotfiles → the checkout's .dotfiles (edits follow the repo) +# ~/.fzf.zsh fzf's key bindings from the Nix store (core.zsh looks +# in /usr/share/fzf, which does not exist here) +# ~/.zsh/completions generated completions for tools without shipped ones +# tmux plugins from nixpkgs (~/.tmux.conf expects TPM; a shim loads them) +# ~/.config/secretspec the keyring provider +# +# The dotfiles themselves (~/.dotfiles, ~/.tmux.conf, the tool configs under +# ~/.config: starship, bat theme, atuin, fastfetch, cheats, …) are linked by +# modules/home/dotfiles.nix. NixOS side (modules/hosts/laptop/configuration.nix): programs.zsh with the global compinit +# and prompt off (core.zsh and theme.zsh do those), users.<user>.shell. +{ ... }: +{ + flake.homeModules.shell = + { config, pkgs, lib, ... }: + let + # Completions for tools that do not ship their own under share/zsh. + # (uv, jj, gh, atuin, zoxide, eza, bat, fd, rg do; /etc/zshrc puts the + # profiles' site-functions on fpath before core.zsh runs compinit.) + generatedCompletions = pkgs.runCommand "nixdaemon-zsh-completions" { } '' + mkdir -p $out + export HOME=$TMPDIR + ${pkgs.secretspec}/bin/secretspec completions zsh > $out/_secretspec + ''; + + # ~/.tmux.conf ends with `run '~/.tmux/plugins/tpm/tpm'` and lists plugins + # for TPM to clone. nixpkgs has the plugins but not TPM, so each plugin is + # linked where TPM would have put it and this stand-in sources them. + tpmShim = '' + #!${pkgs.bash}/bin/bash + # Stand-in for tmux-plugin-manager (NixDaemon modules/home/shell.nix): the + # plugins come from nixpkgs and are linked into ~/.tmux/plugins; this runs + # each plugin's entry script the way TPM would. prefix+I/U do nothing here; + # add plugins in shell.nix instead. + for f in "$HOME"/.tmux/plugins/*/*.tmux; do + case "$f" in */tpm/*) continue ;; esac + [ -x "$f" ] && "$f" + done + exit 0 + ''; + tmuxPlugin = name: pkg: { + name = ".tmux/plugins/${name}"; + value.source = "${pkg}/share/tmux-plugins/${pkg.pluginName}"; + }; + in + { + home.packages = with pkgs; [ + zsh + tmux + secretspec + ]; + + home.file = { + # zsh: hand over to the dotfiles' ZDOTDIR tree. + ".zshenv".text = '' + # Managed by home-manager (NixDaemon modules/home/shell.nix). The shell + # configuration lives in ~/.dotfiles (→ ~/git/daemon-sec-dotfiles). + export ZDOTDIR="$HOME/.dotfiles" + [[ -r "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env" + ''; + ".fzf.zsh".text = '' + # fzf key bindings (Ctrl-T, Alt-C, Ctrl-R) and completion from the Nix + # store; sourced by ~/.dotfiles/config/core.zsh. Quiet when there is no + # tty (the scripts restore `zle`, which fails outside a terminal). + if [[ -t 0 ]]; then + source ${pkgs.fzf}/share/fzf/key-bindings.zsh + source ${pkgs.fzf}/share/fzf/completion.zsh + else + { source ${pkgs.fzf}/share/fzf/key-bindings.zsh; source ${pkgs.fzf}/share/fzf/completion.zsh; } 2>/dev/null + fi + ''; + ".zsh/completions".source = generatedCompletions; + + ".tmux/plugins/tpm/tpm" = { + text = tpmShim; + executable = true; + }; + } + // builtins.listToAttrs [ + (tmuxPlugin "tmux-resurrect" pkgs.tmuxPlugins.resurrect) + (tmuxPlugin "tmux-continuum" pkgs.tmuxPlugins.continuum) + (tmuxPlugin "tmux-yank" pkgs.tmuxPlugins.yank) + (tmuxPlugin "tmux-open" pkgs.tmuxPlugins.open) + ]; + + xdg.configFile = { + # secretspec (https://secretspec.dev): secrets in the system keyring, which + # gnome-keyring provides and PAM unlocks at login. Per-project + # secretspec.toml files declare what a project needs; `secretspec check` + # prompts for anything missing, `secretspec run -- cmd` injects them. + "secretspec/config.toml".text = '' + [defaults] + provider = "keyring" + profile = "default" + ''; + }; + + # bat only sees a theme after its cache is rebuilt from ~/.config/bat/themes. + home.activation.batCache = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ -d "$HOME/.config/bat/themes/" ]; then + run ${pkgs.bat}/bin/bat cache --build >/dev/null 2>&1 || true + fi + ''; + } + ; +} diff --git a/modules/home/sops.nix b/modules/home/sops.nix @@ -0,0 +1,48 @@ +# modules/home/sops.nix — sops-nix for the user: the same encrypted file, +# decrypted into $XDG_RUNTIME_DIR/secrets.d (symlinked at ~/.config/sops-nix/secrets) +# by a user service at login, readable only by daemonsec. +# +# Use this for secrets that belong to the user's programs (API tokens an app +# reads from a file, an rclone config, …); use modules/hosts/laptop/sops.nix for +# anything a system service needs. +# +# sops.secrets.example = { }; # → ~/.config/sops-nix/secrets/example +# sops.secrets.rclone = { path = "${config.xdg.configHome}/rclone/rclone.conf"; }; +# +# secretspec (modules/home/shell.nix) is the complement: per-project runtime +# secrets pulled from the keyring at `secretspec run`, declared next to the +# project in secretspec.toml, not in this repo. +{ ... }: +{ + flake.homeModules.sops = + { config, inputs, pkgs, lib, ... }: + { + imports = [ inputs.sops-nix.homeManagerModules.sops ]; + + sops = { + defaultSopsFile = ../../secrets/secrets.yaml; + age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt"; + age.sshKeyPaths = [ ]; + gnupg.sshKeyPaths = [ ]; + }; + + home.packages = with pkgs; [ + sops + age + ssh-to-age # turn an ssh-ed25519 key into an age recipient when adding another machine + ]; + + # sops-nix's activation step restarts the sops-nix user unit. That unit is a + # home-manager file (~/.config/systemd/user/sops-nix.service), linked by the + # linkGeneration step, and the user systemd manager only sees new unit + # files after a daemon-reload, which home-manager runs at the very end. With + # the extra steps this config adds, the DAG happened to order sops-nix + # before linkGeneration, so the first switch died with "Unit + # sops-nix.service not found". This entry pins the order: linkGeneration → + # daemon-reload → sops-nix. + home.activation.reloadUserUnitsForSops = lib.hm.dag.entryBetween [ "sops-nix" ] [ "linkGeneration" "installPackages" ] '' + ${pkgs.systemd}/bin/systemctl --user daemon-reload 2>/dev/null || true + ''; + } + ; +} diff --git a/modules/home/ssh.nix b/modules/home/ssh.nix @@ -0,0 +1,51 @@ +# modules/home/ssh.nix — the SSH key and client config, from the flake. +# +# The private key is a sops secret (secrets/secrets.yaml: ssh_id_ed25519; +# `nix-cheat secrets`). At login sops-nix decrypts it with the age key into +# the runtime secrets dir and ~/.config/sops-nix/secrets/ssh_id_ed25519 points +# there; ~/.ssh/config names that path, so a fresh machine has a working key +# as soon as ~/.config/sops/age/keys.txt is restored. The public half is +# plain text in ~/.ssh/id_ed25519.pub (comment daemonsec@nixos; registered on +# gitlab.com as "nixos", auth and signing, and glab's git_protocol is ssh). +# +# A plain copy from before this module may still sit at ~/.ssh/id_ed25519; +# nothing reads it any more. +{ ... }: +{ + flake.homeModules.ssh = + { config, ... }: + let + key = config.sops.secrets.ssh_id_ed25519.path; + in + { + sops.secrets.ssh_id_ed25519 = { }; # → %r/secrets.d/…, mode 0400 (ssh is happy with that) + + home.file.".ssh/id_ed25519.pub".text = '' + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAsXOt8jkVBgL2ANFgkftVfRlswxBvBUM33Tv/bS+I5Z daemonsec@nixos + ''; + + programs.ssh = { + enable = true; + enableDefaultConfig = false; + settings = { + "gitlab.com" = { + User = "git"; + IdentityFile = key; + IdentitiesOnly = "yes"; + }; + "github.com" = { + User = "git"; + IdentityFile = key; + IdentitiesOnly = "yes"; + }; + "*" = { + IdentityFile = key; + AddKeysToAgent = "yes"; + ServerAliveInterval = 30; + HashKnownHosts = "no"; + }; + }; + }; + } + ; +} diff --git a/modules/home/terminal.nix b/modules/home/terminal.nix @@ -0,0 +1,164 @@ +# modules/home/terminal.nix — kitty with DMMono Nerd Font and Rosé Pine (main). +# +# The six patched DMMono TTFs come from the dotfiles checkout (tools.nix links +# ~/git/daemon-sec-dotfiles/assets/fonts/nerd-fonts-dm-mono into +# ~/.local/share/fonts). The family has no Bold, so bold maps to Medium. +# +# Palette rule from the migration prompt: pine #31748f is a fill, never ink, +# so it must not sit in an ANSI foreground slot. The Rosé Pine terminal theme +# puts pine in the green slot; the substitute is PARKED for the owner's +# decision. Until then `ansiGreen` below carries foam, the colour the toolbox +# itself uses wherever pine would have been read as text (bin/install.sh). +# +# tmux-style keys (2026-10-08): ctrl+a is a prefix, like tmux's, with tabs as +# tmux windows and kitty windows as tmux panes: +# +# ctrl+a c new tab (cwd kept) ctrl+a - split below (pane) +# ctrl+a n / p next / previous tab ctrl+a | split right +# ctrl+a 1..9 tab N ctrl+a h j k l focus pane left/down/up/right +# ctrl+a , rename tab ctrl+a H J K L move pane +# ctrl+a & close tab ctrl+a o next pane +# ctrl+a x close pane ctrl+a z zoom pane (stack layout toggle) +# ctrl+a [ copy mode (scrollback in Neovim: v y, Enter, q) +# ctrl+a ] paste clipboard ctrl+a space next layout +# ctrl+a { } swap pane back / forth ctrl+a r reload kitty.conf +# ctrl+a u pick a URL (hints) ctrl+a f pick a path (hints) +# ctrl+a ctrl+a send a real ctrl+a to the shell (beginning-of-line) +# ctrl+a shift+arrows resize pane ctrl+a = reset pane sizes +# +# Copy mode is modules/home/kitty/scrollback.lua: the scrollback opens in a bare +# Neovim (no AstroNvim config) with colours, vi motions and search; y copies +# to the clipboard, Enter copies and leaves, q or Esc leaves. +{ ... }: +{ + flake.homeModules.terminal = + { pkgs, lib, ... }: + let + ansiGreen = "#9ccfd8"; # PARKED: ask before changing; see header + + # kitty substitutes INPUT_LINE_NUMBER, CURSOR_LINE and CURSOR_COLUMN in the + # pager command; the Lua reads them from vim.g. + scrollbackPager = lib.concatStringsSep " " [ + "${pkgs.bash}/bin/bash -c" + "'exec ${pkgs.neovim}/bin/nvim 63<&0 0</dev/null" + "-u ${./kitty/scrollback.lua}" + "-c \"let g:kitty_input_line=INPUT_LINE_NUMBER\"" + "-c \"let g:kitty_cursor_line=CURSOR_LINE\"" + "-c \"let g:kitty_cursor_col=CURSOR_COLUMN\"'" + ]; + + prefix = "ctrl+a"; + tabKeys = lib.listToAttrs (map (n: { + name = "${prefix}>${toString n}"; + value = "goto_tab ${toString n}"; + }) (lib.range 1 9)); + in + { + fonts.fontconfig.enable = true; + + programs.kitty = { + enable = true; + font = { + name = "DMMono Nerd Font"; + size = 10; + }; + settings = { + bold_font = ''family="DMMono Nerd Font" style="Medium"''; + italic_font = ''family="DMMono Nerd Font" style="Italic"''; + bold_italic_font = ''family="DMMono Nerd Font" style="Medium Italic"''; + + # Rosé Pine, main (dark) + foreground = "#e0def4"; + background = "#191724"; + selection_foreground = "#e0def4"; + selection_background = "#403d52"; # highlight med + cursor = "#524f67"; # highlight high + cursor_text_color = "#e0def4"; + url_color = "#c4a7e7"; + + active_border_color = "#eb6f92"; + inactive_border_color = "#6e6a86"; + active_tab_foreground = "#e0def4"; + active_tab_background = "#26233a"; + inactive_tab_foreground = "#6e6a86"; + inactive_tab_background = "#191724"; + + color0 = "#26233a"; + color8 = "#6e6a86"; + color1 = "#eb6f92"; + color9 = "#eb6f92"; + color2 = ansiGreen; + color10 = ansiGreen; + color3 = "#f6c177"; + color11 = "#f6c177"; + color4 = "#9ccfd8"; + color12 = "#9ccfd8"; + color5 = "#c4a7e7"; + color13 = "#c4a7e7"; + color6 = "#ebbcba"; + color14 = "#ebbcba"; + color7 = "#e0def4"; + color15 = "#e0def4"; + + # tmux-like layout: panes via the splits layout, stack = zoom, tabs on a + # bottom status line with their index like tmux's window list. + enabled_layouts = "splits,stack"; + window_border_width = "1pt"; + inactive_text_alpha = "0.8"; + tab_bar_edge = "bottom"; + tab_bar_style = "powerline"; + tab_powerline_style = "slanted"; + tab_title_template = "{index}:{title}"; + active_tab_title_template = "{index}:{title}"; + scrollback_lines = 20000; + scrollback_pager = scrollbackPager; + shell_integration = "enabled"; + # Always zsh, whatever $SHELL the session was started with (a session + # begun before the login shell changed still carries SHELL=bash). + shell = "${pkgs.zsh}/bin/zsh"; + }; + + keybindings = { + # tabs = tmux windows + "${prefix}>c" = "new_tab_with_cwd"; + "${prefix}>n" = "next_tab"; + "${prefix}>p" = "previous_tab"; + "${prefix}>," = "set_tab_title"; + "${prefix}>&" = "close_tab"; + "${prefix}>w" = "select_tab"; + # panes = kitty windows + "${prefix}>-" = "launch --location=hsplit --cwd=current"; + "${prefix}>|" = "launch --location=vsplit --cwd=current"; + "${prefix}>x" = "close_window"; + "${prefix}>o" = "next_window"; + "${prefix}>z" = "toggle_layout stack"; + "${prefix}>space" = "next_layout"; + "${prefix}>h" = "neighboring_window left"; + "${prefix}>j" = "neighboring_window down"; + "${prefix}>k" = "neighboring_window up"; + "${prefix}>l" = "neighboring_window right"; + "${prefix}>shift+h" = "move_window left"; + "${prefix}>shift+j" = "move_window down"; + "${prefix}>shift+k" = "move_window up"; + "${prefix}>shift+l" = "move_window right"; + "${prefix}>{" = "move_window_backward"; + "${prefix}>}" = "move_window_forward"; + "${prefix}>shift+left" = "resize_window narrower 3"; + "${prefix}>shift+right" = "resize_window wider 3"; + "${prefix}>shift+up" = "resize_window taller 3"; + "${prefix}>shift+down" = "resize_window shorter 3"; + "${prefix}>=" = "resize_window reset"; + # copy mode and paste + "${prefix}>[" = "show_scrollback"; + "${prefix}>]" = "paste_from_clipboard"; + "${prefix}>u" = "open_url_with_hints"; + "${prefix}>f" = "kitten hints --type path --program -"; + # misc + "${prefix}>r" = "load_config_file"; + "${prefix}>?" = "kitten show_key -m kitty"; + "${prefix}>${prefix}" = "send_text all \\x01"; + } // tabKeys; + }; + } + ; +} diff --git a/modules/home/tools.nix b/modules/home/tools.nix @@ -0,0 +1,121 @@ +# modules/home/tools.nix — runtime closure for the hand-written toolbox in +# ~/.local/bin, plus the general command-line tools. +# +# ~/.local/bin itself is not managed here on purpose: it is a flat git repo +# (vault README: "git init there afterwards so editing a file edits the live +# command"). NixOS puts it first on PATH (modules/hosts/laptop/toolbox.nix). +{ ... }: +{ + flake.homeModules.tools = + { config, pkgs, lib, ... }: + let + # ns [query]: fuzzy search of nixpkgs (+ NixOS and home-manager options) in + # the terminal, with the package description as the preview. nix-search-tv + # indexes search.nixos.org data locally on first run and refreshes it itself. + # Enter print the attribute name (e.g. to paste into tools.nix) + # ctrl-o open the homepage ctrl-s open the nixpkgs source + # ctrl-y copy the attribute name + ns = pkgs.writeShellScriptBin "ns" '' + nst=${pkgs.nix-search-tv}/bin/nix-search-tv + exec $nst print | ${pkgs.fzf}/bin/fzf \ + --query="$*" --scheme=history --prompt='nix › ' \ + --preview="$nst preview {}" --preview-window='right,60%,border-left,wrap' \ + --bind="ctrl-o:execute-silent(${pkgs.xdg-utils}/bin/xdg-open \$($nst homepage {}))" \ + --bind="ctrl-s:execute-silent(${pkgs.xdg-utils}/bin/xdg-open \$($nst source {}))" \ + --bind="ctrl-y:execute-silent(printf %s {} | ${pkgs.wl-clipboard}/bin/wl-copy)" \ + --header='enter: print · ctrl-y: copy · ctrl-o: homepage · ctrl-s: source' + ''; + + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + cryptography + argon2-cffi + rich + questionary + pikepdf + mutagen + pillow + numpy + pyqt6 + youtube-transcript-api + ]); + in + { + home.packages = with pkgs; [ + pythonEnv + ns + nix-search-tv # `ns`, and `nix-search-tv print|preview` by hand + perl + git + jujutsu + gnutar + zstd + pigz + xz + rsync + rclone + aria2 + p7zip + libarchive + gnupg + openssl + pinentry-gnome3 + ffmpeg + yt-dlp + atomicparsley + gallery-dl + imagemagick + img2pdf + resvg + zathura + calibre + poppler-utils + starship + bat + eza + fd + ripgrep + fzf + zoxide + atuin + jq + curl + wget + gh + fastfetch + wl-clipboard + libnotify + + # General tools (2026-10-08): what the dotfiles' zsh modules look for + # (modern.zsh, core.zsh) and what the stock Omarchy keys expect. + uv # PEP-723 scripts in ~/.local/bin (dcx, dorkforge); managed Pythons work via nix-ld + nodejs + btop + lazygit + lazydocker + tealdeer # `tldr` + dust + duf + procs + difftastic + hyperfine + glow + onefetch + tokei + xh + ncdu + parallel + unzip + zip + tree + file + cbonsai + cmatrix + localsend # phone ↔ laptop file drops on the LAN; port 53317 is open in modules/hosts/laptop/configuration.nix + vesktop # Discord client; its config is linked from the dotfiles (dotfiles.nix) + ]; + + # Browser + programs.floorp.enable = true; + } + ; +} diff --git a/modules/home/yazi.nix b/modules/home/yazi.nix @@ -0,0 +1,135 @@ +# modules/home/yazi.nix — yazi, the terminal file manager, with previews. +# +# Pictures preview inline in kitty (its graphics protocol; yazi draws them +# itself, nothing else needed), video frames via ffmpeg, PDFs via poppler, +# SVG via resvg, archives via 7z, JSON via jq, code with syntax colours, and +# the rest as text. Enter / l on a file opens it: images in imv, video and +# audio in mpv, PDFs and books in zathura, text in $EDITOR, anything else via +# xdg-open; o shows every opener. The toolbox's zimg/zbook/comx/gamex/dux +# are offered where they apply (they live in ~/.local/bin). +# +# y start yazi and cd to where you left it (zsh wrapper) +# T maximise the preview pane (toggle-pane) ! shell here +# <C-e>/<C-y> scroll the preview . toggle hidden +# l / Enter smart-enter: open a file, enter a directory +# Theme: Rosé Pine (modules/home/yazi/flavors). Git status marks via the git plugin. +{ ... }: +{ + flake.homeModules.yazi = + { pkgs, ... }: + { + programs.yazi = { + enable = true; + enableZshIntegration = true; + shellWrapperName = "y"; + extraPackages = with pkgs; [ + ffmpeg + poppler-utils + imagemagick + resvg + p7zip + jq + fd + ripgrep + fzf + zoxide + file + mediainfo + imv + ]; + + settings = { + mgr = { + show_hidden = false; + show_symlink = true; + sort_by = "natural"; + sort_sensitive = false; + sort_dir_first = true; + linemode = "size"; + scrolloff = 5; + }; + preview = { + max_width = 1600; + max_height = 1600; + image_delay = 20; + image_filter = "lanczos3"; + image_quality = 90; + wrap = "yes"; + }; + opener = { + edit = [ { run = ''${EDITOR:-nvim} "$@"''; desc = "Edit"; block = true; for = "unix"; } ]; + open = [ { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } ]; + view-image = [ + { run = ''imv "$@"''; desc = "View (imv)"; orphan = true; for = "unix"; } + { run = ''zimg "$1"''; desc = "View in zathura (zimg)"; orphan = true; for = "unix"; } + { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } + ]; + play = [ + { run = ''mpv "$@"''; desc = "Play (mpv)"; orphan = true; for = "unix"; } + { run = ''mpv --no-video "$@"''; desc = "Play audio only (mpv)"; block = true; for = "unix"; } + ]; + open-pdf = [ + { run = ''zathura "$@"''; desc = "Open (zathura)"; orphan = true; for = "unix"; } + { run = ''xdg-open "$1"''; desc = "Open (system default)"; orphan = true; for = "unix"; } + ]; + open-book = [ + { run = ''zbook "$1"''; desc = "Read in zathura (zbook)"; orphan = true; for = "unix"; } + { run = ''zathura "$@"''; desc = "Open (zathura)"; orphan = true; for = "unix"; } + ]; + open-comic = [ + { run = ''comx "$1"''; desc = "Guided view (comx)"; orphan = true; for = "unix"; } + { run = ''zbook "$1"''; desc = "Panels in zathura (zbook)"; orphan = true; for = "unix"; } + { run = ''7z x -y "$1"''; desc = "Extract here (7z)"; block = true; for = "unix"; } + ]; + extract = [ { run = ''7z x -y "$1"''; desc = "Extract here (7z)"; for = "unix"; } ]; + run-exe = [ + { run = ''gamex run "$1"''; desc = "Run (gamex, dGPU)"; block = true; for = "unix"; } + { run = ''gamex proton "$1"''; desc = "Run with Proton-GE"; block = true; for = "unix"; } + ]; + }; + open = { + rules = [ + { mime = "image/gif"; use = [ "view-image" "open" ]; } + { mime = "image/*"; use = [ "view-image" "open" ]; } + { mime = "video/*"; use = [ "play" "open" ]; } + { mime = "audio/*"; use = [ "play" "open" ]; } + { mime = "application/pdf"; use = [ "open-pdf" "open" ]; } + { mime = "application/epub+zip"; use = [ "open-book" "open" ]; } + { name = "*.cbz"; use = [ "open-comic" "extract" ]; } + { name = "*.cbr"; use = [ "open-comic" "extract" ]; } + { name = "*.exe"; use = [ "run-exe" "open" ]; } + { mime = "application/{zip,gzip,x-tar,x-bzip*,x-7z-compressed,x-rar,x-xz,zstd}"; use = [ "extract" "open" ]; } + { mime = "inode/directory"; use = [ "edit" "open" ]; } + { mime = "text/*"; use = [ "edit" "open" ]; } + { mime = "application/{json,toml,x-ndjson,javascript,x-sh,x-shellscript,xml}"; use = [ "edit" "open" ]; } + { mime = "*"; use = [ "open" "edit" ]; } + ]; + }; + }; + + keymap.mgr.prepend_keymap = [ + { on = [ "!" ]; run = ''shell "$SHELL" --block''; desc = "Open a shell here"; } + { on = [ "<C-e>" ]; run = "seek 5"; desc = "Scroll preview down"; } + { on = [ "<C-y>" ]; run = "seek -5"; desc = "Scroll preview up"; } + { on = [ "l" ]; run = "plugin smart-enter"; desc = "Enter directory or open file"; } + { on = [ "<Enter>" ]; run = "plugin smart-enter"; desc = "Enter directory or open file"; } + { on = [ "T" ]; run = "plugin toggle-pane max-preview"; desc = "Maximise the preview"; } + { on = [ "u" "d" ]; run = "shell -- dux %h %s"; desc = "Copy file(s) to the clipboard as a paste-able file"; } + ]; + + plugins = with pkgs.yaziPlugins; { + inherit full-border git smart-enter toggle-pane; + }; + flavors.rose-pine = ./yazi/flavors/rose-pine.yazi; + theme.flavor = { + dark = "rose-pine"; + light = "rose-pine"; + }; + initLua = '' + require("full-border"):setup() + require("git"):setup() + ''; + }; + } + ; +} diff --git a/home/yazi/flavors/rose-pine.yazi/flavor.toml b/modules/home/yazi/flavors/rose-pine.yazi/flavor.toml diff --git a/home/yazi/flavors/rose-pine.yazi/tmtheme.xml b/modules/home/yazi/flavors/rose-pine.yazi/tmtheme.xml diff --git a/modules/hosts/laptop/configuration.nix b/modules/hosts/laptop/configuration.nix @@ -0,0 +1,156 @@ +# modules/hosts/laptop/configuration.nix — the laptop as a NixOS module +# (self.nixosModules.laptop), assembled from the named modules it imports. +# +# PCSpecialist Valeon II 17 (TongFang GM7RGxM): Ryzen 9 6900HX, Radeon 680M at +# 06:00.0, RTX 3070 Ti Laptop at 01:00.0, 2560x1440@240 panel. Hyprland under +# uwsm, Caelestia Shell from home-manager (modules/home/), or Niri with +# Noctalia Shell (modules/features/desktop/), greetd + tuigreet to log in. +{ self, ... }: +{ + flake.nixosModules.laptop = + { config, pkgs, lib, user, ... }: + { + imports = with self.nixosModules; [ + laptop-hardware + laptop-fan-throttle-guard # dead-GPU-fan workaround (vault gpu-fan-fix/, imported unchanged) + laptop-fan-extras # the one imperative step fanfix install did: the performance profile + laptop-uniwill # the `uniwill` hwmon the guard reads: kernel 6.19 driver built for this kernel + laptop-nvidia + laptop-ssd + laptop-nix-settings # nix daemon settings, caches, nh + laptop-toolbox # envfs, ~/.local/bin on PATH, the touchpad udev rule + laptop-sops # sops-nix: secrets/secrets.yaml → /run/secrets + laptop-fan-cli # fan-ec: root side of the `fan` command (modules/home/fan.nix), passwordless for wheel + workstation # Claude Code / desktop, Obsidian, git, gh, glab + home-manager # the user's home, built with the system (modules/home/) + desktop-options # daemon.desktop.* switches + desktop-hyprland # Hyprland + Caelestia (NixOS side) + desktop-niri # Niri + Noctalia: the wrapped package as the login session + ]; + + # The desktops. Both are installed and chosen at login; set one to false to + # drop it (modules/features/desktop/options.nix). + daemon.desktop = { + hyprland.enable = true; + niri.enable = true; + }; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + networking.hostName = "nixos"; + networking.networkmanager.enable = true; + # LocalSend (modules/home/tools.nix) discovers peers and receives on 53317. + networking.firewall.allowedTCPPorts = [ 53317 ]; + networking.firewall.allowedUDPPorts = [ 53317 ]; + + time.timeZone = "Europe/Isle_of_Man"; + i18n.defaultLocale = "en_US.UTF-8"; + services.xserver.xkb = { + layout = "us"; + options = "compose:caps,shift:both_capslock_cancel"; + }; + + nixpkgs.config.allowUnfree = true; # nvidia, obsidian, claude-desktop + + users.users.${user} = { + isNormalUser = true; + description = "daemon-sec"; + extraGroups = [ "networkmanager" "wheel" ]; + shell = pkgs.zsh; + }; + + ##### Shell ################################################################## + # zsh is the login shell. Its configuration is the dotfiles' ZDOTDIR tree + # (modules/home/shell.nix): core.zsh runs a cached compinit and theme.zsh + # starts starship, so the global compinit and the default prompt stay off. + # /etc/zshrc still puts every profile's share/zsh/site-functions on fpath. + programs.zsh = { + enable = true; + enableGlobalCompInit = false; + promptInit = ""; + }; + + # Binaries that are not built by Nix (uv-managed Pythons and their wheels, + # anything mise or npm downloads) expect /lib64/ld-linux-x86-64.so.2. + programs.nix-ld.enable = true; + + ##### Desktop ################################################################ + # The compositors themselves are features (modules/features/desktop/): this + # section is what every desktop shares — the greeter, portals, polkit, audio. + + # Display manager: greetd with the tuigreet text greeter. Remembers the last + # user and session, so a boot is: password, Enter. No theme engine, no X. + services.greetd = { + enable = true; + useTextGreeter = true; + settings.default_session.command = lib.concatStringsSep " " [ + "${pkgs.tuigreet}/bin/tuigreet" + "--time" + "--remember" + "--remember-session" + "--asterisks" + "--sessions ${config.services.displayManager.sessionData.desktops}/share/wayland-sessions" + ]; + }; + security.pam.services.greetd.enableGnomeKeyring = true; # unlock the keyring at login (Claude desktop uses it) + + security.polkit.enable = true; # agent: hyprpolkitagent user service (modules/home/hyprland.nix) + services.udisks2.enable = true; # udiskie + services.power-profiles-daemon.enable = true; # the fan fix depends on it + services.gnome.gnome-keyring.enable = true; + programs.dconf.enable = true; + + services.pulseaudio.enable = false; + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + wireplumber.enable = true; + }; + + programs.firefox.enable = true; + services.printing.enable = true; + + programs.gnupg.agent = { + enable = true; + pinentryPackage = pkgs.pinentry-gnome3; + }; + + ##### Fonts ################################################################## + # DMMono Nerd Font is not in nixpkgs; modules/home/tools.nix links it from + # ~/git/daemon-sec-dotfiles into ~/.local/share/fonts. + fonts.packages = with pkgs; [ + noto-fonts + noto-fonts-color-emoji + noto-fonts-cjk-sans + rubik # Caelestia clock font + material-symbols # Caelestia icons + ]; + fonts.fontconfig.defaultFonts = { + monospace = [ "DMMono Nerd Font" "Noto Sans Mono" ]; + sansSerif = [ "Noto Sans" ]; + serif = [ "Noto Serif" ]; + emoji = [ "Noto Color Emoji" ]; + }; + + ##### Session environment #################################################### + # uwsm imports these through the login shell. GPU-specific ones are in nvidia.nix. + environment.sessionVariables = { + ELECTRON_OZONE_PLATFORM_HINT = "auto"; + GDK_BACKEND = "wayland,x11"; + QT_QPA_PLATFORM = "wayland;xcb"; + QT_WAYLAND_DISABLE_WINDOWDECORATION = "1"; + }; + + environment.systemPackages = with pkgs; [ + pciutils # lspci + usbutils + ]; + + system.stateVersion = "26.05"; + } + ; +} diff --git a/modules/hosts/laptop/default.nix b/modules/hosts/laptop/default.nix @@ -0,0 +1,17 @@ +# modules/hosts/laptop/default.nix — the system this laptop boots. +# +# nh os switch (NH_FLAKE = ~/NixDaemon, picked by hostname) +# sudo nixos-rebuild switch --flake ~/NixDaemon#nixos (the plain way) +# +# Everything the machine is comes from self.nixosModules.laptop (configuration.nix). +{ self, inputs, ... }: +{ + flake.nixosConfigurations.nixos = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + inherit inputs; + user = "daemonsec"; + }; + modules = [ self.nixosModules.laptop ]; + }; +} diff --git a/modules/hosts/laptop/fan-cli.nix b/modules/hosts/laptop/fan-cli.nix @@ -0,0 +1,127 @@ +# modules/hosts/laptop/fan-cli.nix — root side of the `fan` command (modules/home/fan.nix). +# +# `fan-ec` talks to the embedded controller the way fanfix does (same +# acpi_call recipe, same registers; see ~/.local/bin/fanfix and the TUXEDO +# driver), but it is a fixed script in the Nix store, so the wheel group may +# run it through sudo without a password. That is what lets the watchdog in +# fan.nix put the fans back to EC-automatic from a background unit, where +# sudo could not ask for one. fanfix itself lives in the user-writable +# ~/.local/bin and must never get such a rule. +# +# fan-ec status mode, duty %, EC flags fan-ec max 100 % (manual) +# fan-ec auto hand control back to the EC fan-ec <30-100> fixed % (manual) +# fan-ec mode one word: auto | manual | curve-daemon +# +# Manual mode on this laptop makes the EC assert PROCHOT (all cores 399 MHz) +# under load (fan-throttle-guard.nix, line ~97). fan-ec does not try to +# prevent that; the user-side watchdog detects the clamp and calls `fan-ec auto`. +{ ... }: +{ + flake.nixosModules.laptop-fan-cli = + { pkgs, lib, ... }: + let + fan-ec = pkgs.writeShellScriptBin "fan-ec" '' + set -uo pipefail + [ "$(id -u)" = 0 ] || { echo "fan-ec: run as root (sudo fan-ec …)" >&2; exit 1; } + PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.kmod pkgs.systemd ]}:$PATH + + ACPI_CALL=/proc/acpi/call + EC_DEV='\_SB.INOU' + FAN_UNIT=fanfix-fan.service + FAN_MIN_PCT=30 + + ec_ready() { [ -w "$ACPI_CALL" ] || modprobe acpi_call 2>/dev/null; [ -w "$ACPI_CALL" ]; } + ec_raw() { printf '%s' "$1" > "$ACPI_CALL" || return 1; local o; o=$(tr -d '\0\n\r ' < "$ACPI_CALL"); printf '%s' "$o"; } + ec_read() { local out; out=$(ec_raw "$(printf '%s.ECRR 0x%04x' "$EC_DEV" "$1")") || return 1 + [[ "$out" =~ ^0x[0-9a-fA-F]+$ ]] || { echo "EC read $(printf '0x%04x' "$1") failed: $out" >&2; return 1; } + echo $(( out )); } + ec_write() { local out; out=$(ec_raw "$(printf '%s.ECRW 0x%04x 0x%02x' "$EC_DEV" "$1" "$2")") || return 1 + case "$out" in Error*|"not called"|"") echo "EC write $(printf '0x%04x' "$1") failed: ''${out:-no result}" >&2; return 1;; esac + sleep 0.005; } + ec_set_bits() { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v | $2 )); } + ec_clear_bits() { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v & ~$2 & 0xff )); } + ec_bit() { local v; v=$(ec_read "$1") || return 1; echo $(( (v >> $2) & 1 )); } + + R_AP_OEM=0x0741; R_FAN_MODE=0x0751; R_PWM1=0x075B; R_PWM2=0x075C + R_FAN_CTRL=0x078E; R_TBL_SPLIT=0x07C5; R_TBL_ENABLE=0x07C6 + R_CPU_TBL_END=0x0F00; R_CPU_TBL_START=0x0F10; R_CPU_TBL_SPEED=0x0F20 + R_GPU_TBL_END=0x0F30; R_GPU_TBL_START=0x0F40; R_GPU_TBL_SPEED=0x0F50 + R_PWM1_W=0x1804; R_PWM2_W=0x1809 + + universal_ctrl() { ec_bit $R_FAN_CTRL 6; } + tables_enabled() { ec_bit $R_TBL_ENABLE 2; } + pct_to_duty() { echo $(( $1 * 200 / 100 )); } + duty_to_pct() { echo $(( $1 * 100 / 200 )); } + + fan_init_tables() { + local i + ec_clear_bits $R_FAN_MODE 0x40 + [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] || ec_set_bits $R_TBL_SPLIT 0x80 + ec_write $R_CPU_TBL_END 115; ec_write $R_CPU_TBL_START 0; ec_write $R_CPU_TBL_SPEED 1 + ec_write $R_GPU_TBL_END 120; ec_write $R_GPU_TBL_START 0; ec_write $R_GPU_TBL_SPEED 1 + for i in $(seq 1 15); do + ec_write $(( R_CPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_CPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_CPU_TBL_SPEED + i )) 200 + ec_write $(( R_GPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_GPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_GPU_TBL_SPEED + i )) 200 + done + [ "$(tables_enabled)" = 1 ] || ec_set_bits $R_TBL_ENABLE 0x04 + } + fan_apply_duty() { + local d=$1 + if [ "$(universal_ctrl)" = 1 ]; then + [ "$(tables_enabled)" = 1 ] && [ "$(ec_bit $R_FAN_MODE 6)" = 0 ] || fan_init_tables + ec_write $R_CPU_TBL_SPEED "$d"; ec_write $R_GPU_TBL_SPEED "$d" + ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d" + else + local i; [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || ec_set_bits $R_FAN_MODE 0x40 + for i in 1 2 3 4 5 6 7 8 9 10; do ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d"; sleep 0.01; done + fi + } + fan_set_auto() { + if [ "$(universal_ctrl)" = 1 ]; then + [ "$(tables_enabled)" = 1 ] && ec_clear_bits $R_TBL_ENABLE 0x04 + [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] && ec_clear_bits $R_TBL_SPLIT 0x80 + fi + [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] && ec_clear_bits $R_FAN_MODE 0x40 + return 0 + } + mode_word() { + if systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null; then echo curve-daemon; return; fi + if [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || [ "$(tables_enabled)" = 1 ]; then echo manual; else echo auto; fi + } + stop_daemon() { systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null && systemctl stop "$FAN_UNIT"; return 0; } + guard() { ec_ready || { echo "fan-ec: acpi_call not available" >&2; exit 1; } + ec_read $R_AP_OEM >/dev/null || { echo "fan-ec: EC not reachable via $EC_DEV.ECRR" >&2; exit 1; }; } + + case "''${1:-status}" in + mode) guard; mode_word ;; + status) guard + printf 'mode=%s duty=%s%% target=%s%% fan-abnormal=%s full-fan-bit=%s tables=%s\n' \ + "$(mode_word)" "$(duty_to_pct "$(ec_read $R_PWM1)")" \ + "$([ "$(tables_enabled)" = 1 ] && duty_to_pct "$(ec_read $R_CPU_TBL_SPEED)" || echo -)" \ + "$(ec_bit $R_AP_OEM 5)" "$(ec_bit $R_FAN_MODE 6)" "$(tables_enabled)" ;; + auto) guard; stop_daemon; fan_set_auto && echo "fans: auto (EC curve)" ;; + max) guard; stop_daemon; fan_apply_duty 200 && echo "fans: 100 % (manual)" ;; + [0-9]*) p=''${1%\%}; [[ "$p" =~ ^[0-9]+$ ]] && [ "$p" -le 100 ] && [ "$p" -ge "$FAN_MIN_PCT" ] \ + || { echo "fan-ec: percent must be $FAN_MIN_PCT-100 (below that the EC trips at 79 °C)" >&2; exit 2; } + guard; stop_daemon; fan_apply_duty "$(pct_to_duty "$p")" && echo "fans: $p % (manual)" ;; + *) echo "usage: fan-ec status|mode|auto|max|<30-100>" >&2; exit 2 ;; + esac + ''; + in + { + environment.systemPackages = [ fan-ec ]; + + # wheel may run fan-ec without a password: it is immutable store content + # (via the system profile symlink, which is root-owned), does one thing, + # and the watchdog has no terminal to type into. + security.sudo.extraRules = [ + { + groups = [ "wheel" ]; + commands = [ + { command = "/run/current-system/sw/bin/fan-ec"; options = [ "NOPASSWD" ]; } + ]; + } + ]; + } + ; +} diff --git a/modules/hosts/laptop/fan-extras.nix b/modules/hosts/laptop/fan-extras.nix @@ -0,0 +1,30 @@ +# modules/hosts/laptop/fan-extras.nix +# +# The part of the fan fix that `fanfix install` did imperatively on Arch and +# that fan-throttle-guard.nix (imported unchanged) does not carry: the +# "performance" power profile. power-profiles-daemon persists the choice, so +# this oneshot is idempotent; it runs after ppd is up and then re-applies the +# tmpfiles clock floor, because a profile switch rewrites per-policy boost and +# can lift scaling_max_freq (fanfix re-runs tmpfiles for the same reason; the +# stability guard would also catch it within a second). +{ ... }: +{ + flake.nixosModules.laptop-fan-extras = + { pkgs, lib, ... }: + { + systemd.services.fanfix-performance-profile = { + description = "fanfix: select the performance power profile and re-assert the clock floor"; + after = [ "power-profiles-daemon.service" "systemd-tmpfiles-setup.service" ]; + requires = [ "power-profiles-daemon.service" ]; + # graphical.target, not multi-user: on NixOS power-profiles-daemon is itself + # ordered After=multi-user.target, so multi-user here is an ordering cycle. + wantedBy = [ "graphical.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.power-profiles-daemon}/bin/powerprofilesctl set performance"; + ExecStartPost = "${pkgs.systemd}/bin/systemd-tmpfiles --create --prefix=/sys/devices/system/cpu"; + }; + }; + } + ; +} diff --git a/hosts/laptop/fan-reference/99-cpu-freq-cap.conf b/modules/hosts/laptop/fan-reference/99-cpu-freq-cap.conf diff --git a/hosts/laptop/fan-reference/README.md b/modules/hosts/laptop/fan-reference/README.md diff --git a/hosts/laptop/fan-reference/fan-ctl b/modules/hosts/laptop/fan-reference/fan-ctl diff --git a/hosts/laptop/fan-reference/fan-state b/modules/hosts/laptop/fan-reference/fan-state diff --git a/hosts/laptop/fan-reference/fanfix-fan.service b/modules/hosts/laptop/fan-reference/fanfix-fan.service diff --git a/hosts/laptop/fan-reference/fanfix.conf b/modules/hosts/laptop/fan-reference/fanfix.conf diff --git a/hosts/laptop/fan-reference/modules-load.conf b/modules/hosts/laptop/fan-reference/modules-load.conf diff --git a/hosts/laptop/fan-reference/motherboard-stability.service b/modules/hosts/laptop/fan-reference/motherboard-stability.service diff --git a/hosts/laptop/fan-reference/uniwill-laptop.conf b/modules/hosts/laptop/fan-reference/uniwill-laptop.conf diff --git a/modules/hosts/laptop/fan-throttle-guard.nix b/modules/hosts/laptop/fan-throttle-guard.nix @@ -0,0 +1,127 @@ +# modules/hosts/laptop/fan-throttle-guard.nix +# +# Dead-GPU-fan workaround for the PCSpecialist Valeon II 17 (TongFang GM7RGxM, +# Ryzen 9 6900HX + RTX 3070 Ti Laptop). The embedded controller sees the dead +# "Secondary" fan (fan2: 0 rpm while commanded 100 %), raises its fan-abnormal +# flag and, once Tctl reaches ~79 °C, asserts PROCHOT and pins all 16 threads at +# 399 MHz until ~47 °C. That policy is firmware; Linux cannot switch it off. +# The fix is to keep the CPU from ever reaching the trip point: +# +# 1. a static scaling_max_freq floor of 3.2 GHz applied by tmpfiles at boot +# (3.2 GHz ≈ base clock → ~67-70 °C under all-core load, no trips), +# 2. a staged guard (3200 → 2400 → 1800 MHz) driven by k10temp + the uniwill +# board sensor, which also covers the "latched" low-temperature clamp, +# 3. the surviving CPU fan held at 60 % duty through the EC's own ACPI +# methods (acpi_call → \_SB.INOU.ECRR/ECRW, TUXEDO register recipe), +# 4. power-profiles-daemon kept on, profile "performance", and the global +# cpufreq boost flag left at 1 — never use boost=0, ppd 0.30 writes +# per-policy boost on every switch and fails with EINVAL otherwise. +# +# Everything here was measured on the Arch install this was captured from +# (fanfix 2026-08-23, stability guard 2026-09-07). Files beside this module: +# fanfix the CLI/daemon (bash) ← copied verbatim +# stability_guard.py the staged ceiling (python3) ← copied verbatim +# fan-ctl, fan-state bar-widget helpers; need a polkit agent and a bar slot +# +# Verify on first boot: fanfix status · fanfix fan status · fanfix test 30 +# expected: cap 3200 MHz, boost 1, profile performance, no THROTTLE event, +# peak < 75 °C. If a trip still happens: lower the floor to 3000000 below. +{ ... }: +{ + flake.nixosModules.laptop-fan-throttle-guard = + { config, pkgs, lib, ... }: + + let + # fanfix is plain bash; wrap it so the shebang resolves and PATH is supplied + # by the unit (fanDeps) rather than by whatever shell invoked it. + fanfix = pkgs.writeShellScriptBin "fanfix" (builtins.readFile ./fanfix); + + fanDeps = with pkgs; [ + coreutils gnugrep gawk gnused procps util-linux + kmod # modprobe acpi_call / uniwill-laptop + systemd # systemctl, systemd-tmpfiles + power-profiles-daemon # powerprofilesctl + ]; + + capKhz = 3200000; # the floor. 3000000 is the documented fallback. + in + { + ##### 1. EC access and fan/temperature readout ############################## + # acpi_call is out-of-tree (nixpkgs: linuxPackages.acpi_call). uniwill-laptop + # is in-tree; `force=1` is required because the DMI match list does not carry + # this GM7RGxM. Verify `modinfo uniwill-laptop` exists on the chosen kernel. + boot.extraModulePackages = [ config.boot.kernelPackages.acpi_call ]; + boot.kernelModules = [ "acpi_call" "uniwill-laptop" ]; + boot.extraModprobeConfig = '' + options uniwill-laptop force=1 + ''; + + ##### 2. Static floor, applied before any user load exists ################### + systemd.tmpfiles.rules = [ + "w /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq - - - - ${toString capKhz}" + ]; + + ##### 3. power-profiles-daemon stays on ###################################### + services.power-profiles-daemon.enable = true; + + ##### 4. Staged thermal ceiling (replaces /etc/systemd/system/motherboard-stability.service) + systemd.services.motherboard-stability = { + description = "CPU stability limits for the GM7RGxM fan/power fault workaround"; + after = [ "systemd-tmpfiles-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.python3}/bin/python3 -I ${./stability_guard.py}"; + Restart = "on-failure"; + RestartSec = 3; + RuntimeDirectory = "motherboard-stability"; + RuntimeDirectoryMode = "0755"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ "/sys/devices/system/cpu" "/run/motherboard-stability" ]; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = "AF_UNIX"; + LockPersonality = true; + RestrictSUIDSGID = true; + CapabilityBoundingSet = ""; + UMask = "0022"; + }; + }; + + ##### 5. Surviving CPU fan at a fixed 60 % duty ############################## + # CURVE is "temp:pct …" pairs; a flat 0:60 100:60 is what has been running. + # fanfix refuses anything below 30 %. Edit here, not in /etc, then rebuild. + # + # NOT started at boot (wantedBy = []). Measured 2026-10-07 on NixOS: while the + # daemon holds the EC in manual/custom-table fan mode, the EC asserts PROCHOT + # (all cores 399 MHz) the moment any load starts, even at 37 °C. Stopping the + # unit and `fanfix fan auto` cleared it instantly; 10 s all-core test then ran + # at 3112 MHz, peak 53 °C, 0 trips. The 3.2 GHz floor + stability guard are + # enough on their own. Start by hand to experiment: systemctl start fanfix-fan + environment.etc."fanfix.conf".text = '' + CURVE="0:60 100:60" + ''; + + systemd.services.fanfix-fan = { + description = "fanfix: temperature → fan-duty curve for the surviving CPU fan (dead GPU fan workaround)"; + after = [ "multi-user.target" ]; + wantedBy = [ ]; # see note above; manual start only + path = fanDeps; + serviceConfig = { + Type = "simple"; + ExecStart = "${fanfix}/bin/fanfix fan-daemon"; + ExecStopPost = "${fanfix}/bin/fanfix fan-release"; + Restart = "on-failure"; + RestartSec = 5; + }; + }; + + ##### 6. Tools on PATH ###################################################### + environment.systemPackages = [ fanfix pkgs.lm_sensors ] ++ fanDeps; + } + ; +} diff --git a/hosts/laptop/fanfix b/modules/hosts/laptop/fanfix diff --git a/modules/hosts/laptop/hardware.nix b/modules/hosts/laptop/hardware.nix @@ -0,0 +1,50 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ ... }: +{ + flake.nixosModules.laptop-hardware = + { config, lib, pkgs, modulesPath, ... }: + + { + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; + fsType = "btrfs"; + }; + + fileSystems."/home" = + { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; + fsType = "btrfs"; + options = [ "subvol=home" ]; + }; + + fileSystems."/nix" = + { device = "/dev/disk/by-uuid/e2664576-4e65-4c79-ac1c-9e2fa9e12a26"; + fsType = "btrfs"; + options = [ "subvol=nix" ]; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/D3FC-22C2"; + fsType = "vfat"; + options = [ "fmask=0077" "dmask=0077" ]; + }; + + swapDevices = + [ { device = "/dev/disk/by-uuid/6aee8a42-a2a1-4d18-8f5c-695195e0c122"; } + ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + } + ; +} diff --git a/modules/hosts/laptop/nix-settings.nix b/modules/hosts/laptop/nix-settings.nix @@ -0,0 +1,46 @@ +# modules/hosts/laptop/nix-settings.nix — nix daemon settings and the nh helper. +# Imported by both the `nixos` target and the `bootstrap` stage. +{ ... }: +{ + flake.nixosModules.laptop-nix-settings = + { pkgs, ... }: + { + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + # Hyprland is built from its own flake pins, which cache.nixos.org does + # not have. Without the Hyprland cache every update compiles it locally. + substituters = [ + "https://cache.nixos.org" + "https://hyprland.cachix.org" + ]; + trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc=" + ]; + }; + + # nh, the Nix helper: `nh os switch|boot|test`, `nh clean all`, `nh search`. + # Wraps nixos-rebuild with nix-output-monitor progress and an nvd diff of + # what a generation changes, and asks for sudo only for the switch itself. + # NH_FLAKE points at this repo, so `nh os boot` works from any directory; + # the configuration is picked by hostname (`nixos`), or with -H <name>. + programs.nh = { + enable = true; + flake = "/home/daemonsec/NixDaemon"; + clean = { + enable = true; # weekly `nh clean all`: drops old generations and runs the GC, + dates = "weekly"; # keeping the last 5 and anything newer than 14 days + extraArgs = "--keep 5 --keep-since 14d"; + }; + }; + + # The two tools nh builds on, also useful by hand: + # nvd diff /run/current-system result what a build would change + # nom build .#… nix build with a live tree view + environment.systemPackages = with pkgs; [ + nvd + nix-output-monitor + ]; + } + ; +} diff --git a/modules/hosts/laptop/nvidia.nix b/modules/hosts/laptop/nvidia.nix @@ -0,0 +1,58 @@ +# modules/hosts/laptop/nvidia.nix +# +# The panel (eDP-1) is wired to the RTX 3070 Ti at 01:00.0: the firmware MUX is +# in discrete mode (amdgpu's eDP-2 reports disconnected). Linux cannot change +# the MUX, so this configures what the hardware presents: NVIDIA open kernel +# module with modesetting, the Radeon 680M left as a secondary DRM device. +# +# PARKED: if the MUX is switched to hybrid in the BIOS, swap the AQ_DRM_DEVICES +# order (igpu-card first) and add the prime offload block at the bottom. +{ ... }: +{ + flake.nixosModules.laptop-nvidia = + { config, pkgs, lib, ... }: + { + services.xserver.videoDrivers = [ "nvidia" ]; + + hardware.graphics = { + enable = true; + extraPackages = [ pkgs.nvidia-vaapi-driver ]; + }; + + hardware.nvidia = { + open = true; # GA104 is supported by the open kernel modules + modesetting.enable = true; # nvidia-drm.modeset=1 + package = config.boot.kernelPackages.nvidiaPackages.stable; + nvidiaSettings = false; + # powerManagement.enable = true; # suspend/resume helpers; untested on this laptop, left at default + }; + + # Stable, colon-free names for the two DRM cards. Aquamarine splits + # AQ_DRM_DEVICES on ':', so the /dev/dri/by-path names cannot go in it (the PCI + # address has colons): it chopped them into "pci-0000", "01", "00.0-card", found + # no GPU and Hyprland aborted at startup with "CBackend::create() failed!". + # ID_PATH is matched exactly so the boot-time simpledrm card (whose ID_PATH is + # pci-0000:01:00.0-platform-simple-framebuffer.0) does not take the dGPU name. + services.udev.extraRules = '' + SUBSYSTEM=="drm", KERNEL=="card[0-9]*", ENV{ID_PATH}=="pci-0000:01:00.0", SYMLINK+="dri/dgpu-card" + SUBSYSTEM=="drm", KERNEL=="card[0-9]*", ENV{ID_PATH}=="pci-0000:06:00.0", SYMLINK+="dri/igpu-card" + ''; + + environment.sessionVariables = { + # Stable device order for Hyprland/aquamarine: dGPU (panel) first, iGPU second. + AQ_DRM_DEVICES = "/dev/dri/dgpu-card:/dev/dri/igpu-card"; + LIBVA_DRIVER_NAME = "nvidia"; + __GLX_VENDOR_LIBRARY_NAME = "nvidia"; + NVD_BACKEND = "direct"; + }; + + # Hybrid (iGPU drives the panel, dGPU on demand). Only if the BIOS MUX is set to hybrid: + # hardware.nvidia.prime = { + # offload.enable = true; + # offload.enableOffloadCmd = true; + # amdgpuBusId = "PCI:6:0:0"; + # nvidiaBusId = "PCI:1:0:0"; + # }; + } + ; +} diff --git a/modules/hosts/laptop/sops.nix b/modules/hosts/laptop/sops.nix @@ -0,0 +1,37 @@ +# modules/hosts/laptop/sops.nix — sops-nix: secrets encrypted in this repo, decrypted +# at activation into /run/secrets (root-only tmpfs; per-secret owner/mode). +# +# One age identity does everything (.sops.yaml): the user edits with the sops +# CLI (reads ~/.config/sops/age/keys.txt), the system decrypts at boot with a +# root-only copy at /var/lib/sops-nix/key.txt. Put it there once: +# +# sudo install -D -m 600 -o root -g root ~/.config/sops/age/keys.txt /var/lib/sops-nix/key.txt +# +# No SSH host key is used: sshd is not enabled on this machine, so there is +# none to derive an age key from (sshKeyPaths is emptied below for that reason). +# +# Declaring a secret: +# sops.secrets.my-token = { }; # → /run/secrets/my-token, root:root 0400 +# sops.secrets."ssd.key" = { path = "/etc/secrets/ssd.key"; }; # the LUKS key, once it is +# # added to secrets/secrets.yaml (sops set …) +# sops.secrets.wifi-psk = { owner = user; }; # readable by the user +# then `sops secrets/secrets.yaml` to add the value, and `nh os switch`. +{ ... }: +{ + flake.nixosModules.laptop-sops = + { inputs, user, ... }: + { + imports = [ inputs.sops-nix.nixosModules.sops ]; + + sops = { + defaultSopsFile = ../../../secrets/secrets.yaml; + age = { + keyFile = "/var/lib/sops-nix/key.txt"; + sshKeyPaths = [ ]; + generateKey = false; # the key is the user's (see header), never a fresh one + }; + gnupg.sshKeyPaths = [ ]; + }; + } + ; +} diff --git a/modules/hosts/laptop/ssd.nix b/modules/hosts/laptop/ssd.nix @@ -0,0 +1,35 @@ +# modules/hosts/laptop/ssd.nix +# +# Spare Samsung SSD 980 1 TB (LUKS2, btrfs label SSD) at /mnt/ssd, unlocked at +# boot from /etc/secrets/ssd.key. The key is restored by hand once (vault +# samsung-ssd.md, section 2): +# +# sudo mkdir -p -m 700 /etc/secrets +# gpg -d --pinentry-mode loopback ssd.key.gpg | sudo tee /etc/secrets/ssd.key >/dev/null +# sudo chmod 400 /etc/secrets/ssd.key +# +# `nofail` on both lines: the laptop boots normally while the key (or the +# drive) is missing; the unit just fails. Swap the key path for `none` to type +# the passphrase at boot instead. +{ ... }: +{ + flake.nixosModules.laptop-ssd = + { ... }: + { + environment.etc.crypttab.text = '' + ssd UUID=8cd17d0f-adf3-430f-9d95-d17ef42df9b0 /etc/secrets/ssd.key luks,nofail + ''; + + fileSystems."/mnt/ssd" = { + device = "/dev/mapper/ssd"; + fsType = "btrfs"; + options = [ + "compress=zstd:3" + "noatime" + "nofail" + "x-systemd.device-timeout=10s" + ]; + }; + } + ; +} diff --git a/hosts/laptop/stability_guard.py b/modules/hosts/laptop/stability_guard.py diff --git a/modules/hosts/laptop/toolbox.nix b/modules/hosts/laptop/toolbox.nix @@ -0,0 +1,31 @@ +# modules/hosts/laptop/toolbox.nix +# +# NixOS-side support for the hand-written toolbox that lives, flat, in +# ~/.local/bin (its own git repo; not in the Nix store). Shared by the target +# and the bootstrap configuration. +{ ... }: +{ + flake.nixosModules.laptop-toolbox = + { pkgs, lib, user, ... }: + { + # Several tools keep Arch-style shebangs (#!/bin/bash: fanfix, dropterm, + # omarchy-menu-*; #!/usr/bin/python3: lid-control). envfs resolves those + # paths from the caller's PATH instead of patching the scripts. + services.envfs.enable = true; + + # ~/.local/bin first on PATH (prepends in /etc/set-environment). + environment.localBinInPath = true; + + users.users.${user}.extraGroups = [ + "input" # padx talks to the touchpad over hidraw + "video" + ]; + + # padx: the Pixart 093A:0274 touchpad behind the UNIW0001 I2C-HID bridge. + # Replaces ~/trackpad-fix/60-padx-touchpad.rules from the Arch install. + services.udev.extraRules = '' + KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{modalias}=="hid:b0018g*v0000093Ap00000274", GROUP="input", MODE="0660" + ''; + } + ; +} diff --git a/modules/hosts/laptop/uniwill-laptop.nix b/modules/hosts/laptop/uniwill-laptop.nix @@ -0,0 +1,25 @@ +# modules/hosts/laptop/uniwill-laptop.nix +# +# fan-throttle-guard.nix expects the in-tree `uniwill-laptop` driver (the +# `uniwill` hwmon: fan1/fan2 rpm, pwm1/pwm2, board temps; also the keyboard +# backlight LED and the touchpad-toggle bit padx mentions). Upstream merged it +# in Linux 6.19; the nixpkgs 6.18 kernel predates it and the 7.2 kernel config +# leaves X86_PLATFORM_DRIVERS_UNIWILL off. Building the v6.19 sources as an +# out-of-tree module against whatever boot.kernelPackages selects is the cheap +# fix: a 10-second compile, no custom kernel. Verified to build on 6.18.55. +# +# Without this hwmon stability_guard.py pins the CPU at 1.8 GHz ("essential +# sensor or main fan unavailable"), so this file is not optional. +{ ... }: +{ + flake.nixosModules.laptop-uniwill = + { config, lib, pkgs, ... }: + { + boot.extraModulePackages = [ + (config.boot.kernelPackages.callPackage ./uniwill-laptop/_package.nix { }) + ]; + # `boot.kernelModules` and the `force=1` modprobe option (this GM7RGxM is not + # in the driver's DMI list) are set in fan-throttle-guard.nix. + } + ; +} diff --git a/hosts/laptop/uniwill-laptop/Makefile b/modules/hosts/laptop/uniwill-laptop/Makefile diff --git a/hosts/laptop/uniwill-laptop/package.nix b/modules/hosts/laptop/uniwill-laptop/_package.nix diff --git a/hosts/laptop/uniwill-laptop/uniwill-acpi.c b/modules/hosts/laptop/uniwill-laptop/uniwill-acpi.c diff --git a/hosts/laptop/uniwill-laptop/uniwill-wmi.c b/modules/hosts/laptop/uniwill-laptop/uniwill-wmi.c diff --git a/hosts/laptop/uniwill-laptop/uniwill-wmi.h b/modules/hosts/laptop/uniwill-laptop/uniwill-wmi.h diff --git a/modules/parts.nix b/modules/parts.nix @@ -0,0 +1,9 @@ +# modules/parts.nix — flake-parts wiring shared by every module under modules/. +{ inputs, ... }: +{ + systems = [ "x86_64-linux" ]; + imports = [ + inputs.home-manager.flakeModules.home-manager # flake.homeModules / flake.homeConfigurations + inputs.wrapper-modules.flakeModules.default # flake.wrappers, perSystem.wrappers + ]; +} diff --git a/modules/workstation.nix b/modules/workstation.nix @@ -1,43 +0,0 @@ -# Claude Code, the Claude desktop app, Obsidian, and the git / GitHub / GitLab -# command-line tools. Written by nixdaemon-bootstrap.sh; edit freely. -{ lib, pkgs, inputs, ... }: -let - # Anthropic's own Linux builds, repackaged for Nix by numtide and refreshed - # daily. (NixOS isn't a distro Anthropic supports directly: its desktop app - # ships as a .deb for Debian/Ubuntu.) - claude = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}; -in -{ - nix.settings.experimental-features = [ "nix-command" "flakes" ]; - - # Unfree packages (Obsidian) are already allowed elsewhere in this config. - - environment.systemPackages = [ - claude.claude-code # terminal: `claude` - claude.claude-desktop # desktop app: "Claude" in your launcher, or `claude-desktop` - pkgs.obsidian - pkgs.git - pkgs.gh # GitHub CLI - pkgs.glab # GitLab CLI - pkgs.qemu_kvm # only for the desktop app's Cowork tab - ]; - - # The desktop app keeps its sign-in in the system keyring; without one it - # asks you to log in on every launch. - services.gnome.gnome-keyring.enable = lib.mkDefault true; - - # The desktop app's Cowork tab runs its tasks in a local VM, which needs KVM. - # If you don't use Cowork, delete these two lines and qemu_kvm above. - boot.kernelModules = [ "vhost_vsock" ]; - users.groups.kvm.members = [ "daemonsec" ]; - - # Run Electron apps (Claude, Obsidian) natively on Wayland, e.g. on Hyprland. - environment.sessionVariables.NIXOS_OZONE_WL = lib.mkDefault "1"; - - # Optional: numtide's binary cache, so the Claude packages download instead - # of building a couple of small helper tools locally on each update. - # nix.settings.extra-substituters = [ "https://cache.numtide.com" ]; - # nix.settings.extra-trusted-public-keys = [ - # "niks3.numtide.com-1:DTx8wZduET09hRmMtKdQDxNNthLQETkc/yaX7M4qK0g=" - # ]; -} diff --git a/scripts/wrap.sh b/scripts/wrap.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# scripts/wrap.sh FILE ATTR — turn a plain NixOS / home-manager module file +# into a flake-parts module that exports it under ATTR, in place: +# +# # leading comment block, kept as is +# { ... }: +# { +# ATTR = +# <the original module, indented by two spaces>; +# } +# +# The split is at the first line that is neither a `#` comment nor blank, so +# the file's header comment stays at the top where editors and readers expect it. +set -euo pipefail +file=$1 +attr=$2 +tmp=$(mktemp) +awk -v attr="$attr" ' + !body && ($0 ~ /^#/ || $0 ~ /^[[:space:]]*$/) { print; next } + !body { body = 1; print "{ ... }:"; print "{"; print " " attr " =" } + body { if ($0 ~ /^[[:space:]]*$/) print ""; else print " " $0 } + END { print " ;"; print "}" } +' "$file" > "$tmp" +mv "$tmp" "$file"