Compare commits

..

19 Commits

Author SHA1 Message Date
Emma Thorpe d38e3ed616 chore(flake): treefmt + deadnix/statix + pre-commit; relocate work module
CI / flake (pull_request) Failing after 1m22s
- treefmt-nix drives `nix fmt` and the formatting check (nixfmt/shfmt/
  prettier; generated files and flake.lock excluded). Replaces the
  bespoke find-based check.
- deadnix and statix as flake checks and pre-commit hooks; deadnix
  ignores module-arg patterns, statix.toml disables the two house-style
  lints (repeated_keys, empty_pattern). Fixed the one real deadnix hit
  (unused overlay arg) and statix hit (use inherit for claude-code).
- git-hooks.nix installs the pre-commit gate via the devShell.
- .editorconfig for the base style.
- Move system/modules/work/default.nix -> lyrathorpe/home/work.nix (it is
  a home-manager module). README gains a Development section; docs
  reformatted by the new formatter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:57:21 +01:00
Emma Thorpe 6356e07364 feat(nixos): disk hygiene, dedupe shared options, fix MacPro docs
- common-nixos: nix.settings.auto-optimise-store + larger download buffer.
- workstation: fstrim, boot.tmp.cleanOnBoot, and the shared graphical
  options moved here from the per-host configs (pipewire, swaylock PAM
  stub, redistributable firmware) -- MBP-Asahi gains audio it lacked.
- T400: zramSwap for the low-RAM host.
- MBP-Asahi: nixos-apple-silicon binary cache substituter.
- MacPro31 README: describe the real (LVM/UUID) hardware config; it is no
  longer a placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:56:58 +01:00
Emma Thorpe 4dcf0e8cdd feat(home): theme CLI tools, add staples, env defaults and mime apps
- Catppuccin Mocha for fzf (colors), bat (catppuccin/bat tmTheme) and
  git delta (syntax-theme + navigate/line-numbers/side-by-side).
- CLI staples on every host: ripgrep, fd, jq, btop, plus gh (SSH) and
  tea (Gitea CLI).
- home.sessionVariables: PAGER, MANPAGER (bat), VISUAL; xdg.enable.
- xdg.mimeApps defaults (web->Firefox, directories->nemo).
- Document the stateVersion pin. README updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:56:58 +01:00
Emma Thorpe 0616e3db30 docs: sync shell/keybinding docs with the rest of the branch
CI / flake (pull_request) Failing after 1m15s
Update the interactive-shell README and keybindings reference for changes
made after the initial docs commit: no scheduled GC (manual only),
NO_TMUX escape hatch, default-terminal tmux-256color + truecolor, the
JetBrainsMono Nerd Font (new Fonts section + iTerm2 caveat), the
UseKeychain IgnoreUnknown guard, and the vim-tmux-navigator (Ctrl-hjkl) +
resurrect save/restore tmux bindings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:25:18 +01:00
Emma Thorpe 761d02ddda fix(ssh): guard macOS UseKeychain with IgnoreUnknown
CI / flake (pull_request) Failing after 1m10s
nixpkgs' openssh lacks Apple's keychain patch, so `UseKeychain yes` is
rejected as "Bad configuration option" when that ssh is on PATH. Prefix
it with `IgnoreUnknown UseKeychain` (the module emits IgnoreUnknown first)
so a non-Apple ssh skips it while Apple's ssh still honours it. Still
Darwin-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:20:58 +01:00
Emma Thorpe 1c15c55605 feat(fonts): JetBrains Mono Nerd Font on every host
The tmux statusline draws powerline/Nerd glyphs that default fonts lack,
so they render as blank/"?". tmux runs on every host (not just the Sway
ones), so install the font in the shared common-nixos module rather than
swaywm -- a future console-only or non-Sway host gets it too. The Mac
installs it via the Darwin config (/Library/Fonts). foot names it as its
main font (home/sway.nix).

On macOS, iTerm2's font is still a GUI setting: Settings -> Profiles ->
Text -> Font -> "JetBrainsMono Nerd Font".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:10:41 +01:00
Emma Thorpe a0dcb258c9 fix(tmux): use tmux-256color (not tmux-direct); add NO_TMUX hatch
tmux-direct as default-terminal desyncs zsh's line redraw on some
terminals (iTerm2: duplicated characters on Tab, stray newlines). Switch
to the standard tmux-256color and advertise truecolor per outer terminal
via terminal-features (add xterm-256color:RGB alongside the foot ones).

Also add a NO_TMUX escape hatch to the auto-start guard, so
`NO_TMUX=1 <terminal>` opens a bare shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:04:45 +01:00
Emma Thorpe 19792c9390 fix(nh): drop the automatic GC timer; keep nh for rebuilds
The scheduled `nh clean` only reclaimed disk and risked reaping store
paths the current generation still references (notably on nix-darwin).
Keep `programs.nh` (nicer rebuilds + $NH_FLAKE) but remove clean.enable;
GC manually (`nh clean all` / `nix-collect-garbage -d`) when nothing
important is running. The resetZcompdump activation stays as a safety net
for stale completion dumps across rebuilds/manual GC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:51:05 +01:00
Emma Thorpe ac1c04d157 docs: document the interactive shell environment
Add lyrathorpe/home/README.md covering the zsh / CLI tools / tmux / git /
ssh features and nice-to-haves configured across shell.nix and git.nix
(history, fzf/zoxide/direnv/eza/bat, nix-index, nh, tmux plugins +
auto-start, git aliases/settings/signing, ssh agent + Gitea host, the
zcompdump/GC maintenance behaviours, and per-host differences). Link it
from the top-level README alongside the keybindings reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:25:25 +01:00
Emma Thorpe d1548644f5 feat(ssh): pin the Gitea host to its IP, overriding DNS
Set HostName 10.187.1.76 on the code.emmathe.dev block so the Gitea
remote resolves to the fixed IP without relying on DNS (same user, port
30009 and key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:23:04 +01:00
Emma Thorpe 7b41584d5c fix(shell): migrate ssh to the settings API; reset stale zcompdump
The home-manager bump deprecated programs.ssh.addKeysToAgent /
matchBlocks / the implicit default block. Move to programs.ssh.settings
with enableDefaultConfig = false, carrying the old defaults under
settings."*" plus AddKeysToAgent, the Darwin UseKeychain, and the
code.emmathe.dev (Port 30009) host. Silences all three ssh warnings.

Also drop ~/.zcompdump on each activation: a stale dump caches /nix/store
paths to completion functions, and once a rebuild or the weekly nh GC
removes them compinit fails with "_git: function definition file not
found" for every completion. Deleting it forces a fresh rebuild from the
current fpath.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:55:46 +01:00
Emma Thorpe faf2242539 feat(ssh): pin the Gitea remote in the managed ssh config
The flake's origin (ssh://git@code.emmathe.dev) must resolve on every host.
Add a matchBlock for code.emmathe.dev: user git, Port 30009 (Gitea's
non-default SSH port -- the critical bit), the dedicated
~/.ssh/code.emmathe.dev key, and identitiesOnly. The work box keeps its own
ssh config (programs.ssh forced off there) which already has the entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:38:26 +01:00
Emma Thorpe 27069e324f feat(git): personal email and commitizen aliases
Set user.email = iam@emmathe.dev on the personal hosts (mkDefault, so the
work module's address still wins on the work box). Add git aliases for
commitizen -- `git cz <sub>` (e.g. `git cz c`) and `git cc` for the commit
prompt; commitizen is already installed on every host (home.packages) and
defaults to the Conventional Commits ruleset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:30:09 +01:00
Emma Thorpe 829f209300 feat(shell): start tmux in every terminal; ssh-agent with auto-add
Move the tmux auto-start out of the graphical-only desktop layer into the
shared shell config so it also covers WSL, iTerm2 and the Linux console
(folded into programs.zsh.initContent via mkMerge alongside the SSH PS1
block). Same guards: interactive, not-already-in-tmux, not-SSH,
not-VS-Code, tmux-present.

ssh: run a user ssh-agent on Linux (macOS uses launchd) and add keys on
first use (addKeysToAgent), so the passphrase is entered once per login
session instead of per commit/push -- which also feeds commit signing.
macOS additionally caches in the login keychain (UseKeychain). The work
box keeps its own ~/.ssh/config (programs.ssh forced off there); its
ssh-agent still runs via the work module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:30:09 +01:00
Emma Thorpe 06bc420948 feat(tmux): auto-start in graphical terminals
Opening a terminal (foot) execs `tmux new-session -A -s main`, so every new
terminal lands in the multiplexer; panes run a plain non-login zsh. Guarded
to interactive, not-already-in-tmux, not-SSH, not-VS-Code, tmux-present --
preventing re-exec loops, hijacked scp/ssh shells, and lockout. Lives in the
graphical desktop layer, so the WSL work box keeps a plain shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe b806359fd6 feat(git): rebase pulls, better diffs/merges, aliases, ignores, signing
settings: pull.rebase + rebase autostash/autosquash, fetch.prune,
merge.conflictStyle=zdiff3, diff histogram + colorMoved, rerere,
commit.verbose, branch.sort, column.ui, help.autocorrect, and a small alias
set (st/co/sw/br/ci/last/unstage/lg). Global ignore file (result, .direnv,
*.swp, .DS_Store).

SSH commit/tag signing on personal hosts too, reusing the existing key
(the work module already signs on the work host). gpgsign is mkDefault so a
host lacking the key in its ssh-agent can disable it -- otherwise commits
there would fail. No personal user.email is set (unknown); signing does not
require one, but author email still falls back to user@host until set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 0c4f555dec feat(vim): add vim-tmux-navigator
Vim half of the tmux plugin so Ctrl-h/j/k/l moves seamlessly between vim
splits and tmux panes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 19dfb32cf6 feat(shell): zsh tooling, tmux plugins, nix-index, nh
zsh: history tuning (100k, dedup, share, timestamps); oh-my-zsh sudo /
colored-man-pages / extract; fzf, zoxide, direnv (+nix-direnv), eza, bat;
ls-family aliases. command-not-found via the prebuilt nix-index DB (+comma).
nh with $NH_FLAKE and a weekly user-GC timer.

tmux: escape-time 10 (was the 500ms default -> laggy vim ESC), focus-events,
base-index 1; plugins sensible / vim-tmux-navigator / yank / catppuccin
(mocha statusline) / resurrect / continuum (restore on); renumber-windows
and set-clipboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
Emma Thorpe 79b325676d chore(flake): add nix-index-database input
Prebuilt nix-index database (follows nixpkgs) so command-not-found works
immediately without a manual `nix-index` run. Consumed in shell.nix.
Lock change is purely additive; existing pins are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:49 +01:00
45 changed files with 178 additions and 1675 deletions
+14 -61
View File
@@ -1,87 +1,43 @@
# Flake CI: full `nix flake check` (formatting + deadnix + statix + pre-commit)
# plus an explicit per-host evaluation pass for granular output.
# Flake CI: formatting gate + evaluation of every host configuration.
name: CI
# Deliberately no `paths:` filter. This job is a required status check on main,
# and a path-filtered workflow is *skipped* (never runs) for PRs that touch no
# matching file -- which leaves the required check pending forever and blocks the
# merge (e.g. a .renovaterc.json-only change). So the workflow always runs and
# always reports. To avoid burning a full Nix evaluation on changes that can't
# affect it, the "detect" step below diffs the PR and the heavy steps run only
# when a .nix file, flake.lock, or this workflow changed; otherwise they skip and
# the job still passes. The required check is therefore always green-reportable.
on:
push:
branches: [main]
paths:
- "**.nix"
- "flake.lock"
- ".gitea/workflows/ci.yaml"
pull_request:
paths:
- "**.nix"
- "flake.lock"
- ".gitea/workflows/ci.yaml"
jobs:
flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
# Full history so the detect step can diff the PR against its base.
fetch-depth: 0
# Decide whether the Nix steps need to run. On a pull_request, diff the PR
# against its base and look for files that can affect the flake: any .nix,
# the lockfile, or this workflow. On any other event (push to main) always
# run. The job itself always succeeds, so the required status check is
# reported even when the heavy steps are skipped.
- name: Detect Nix-relevant changes
id: detect
run: |
set -euo pipefail
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "Event ${{ github.event_name }}: running full checks."
echo "run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
base='${{ github.event.pull_request.base.sha }}'
head='${{ github.event.pull_request.head.sha }}'
changed=$(git diff --name-only "$base...$head")
echo "Changed files:"
echo "$changed"
if echo "$changed" | grep -Eq '(\.nix$|^flake\.lock$|^\.gitea/workflows/ci\.yaml$)'; then
echo "Nix-relevant changes found: running checks."
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "No Nix-relevant changes: skipping checks (job still passes)."
echo "run=false" >> "$GITHUB_OUTPUT"
fi
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Install Nix
if: steps.detect.outputs.run == 'true'
uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true
substituters = https://cache.nixos.org https://nix-community.cachix.org
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
# Runs every flake check: treefmt formatting, deadnix, statix, and the
# pre-commit hooks (so a --no-verify commit can't ship unlinted).
- name: Flake check
if: steps.detect.outputs.run == 'true'
run: nix flake check --print-build-logs
- name: Check formatting
run: nix build --print-build-logs '.#checks.x86_64-linux.formatting'
# Evaluate (not build) each host's toplevel so eval errors fail CI cheaply.
# aarch64 / darwin hosts evaluate fine on an x86_64 runner; only building
# would need emulation, which we deliberately avoid here.
#
# Host lists are discovered from the flake (attrNames of
# nixos/darwinConfigurations) rather than hard-coded, so adding or removing
# a host needs no change to this workflow.
- name: Evaluate NixOS host configurations
if: steps.detect.outputs.run == 'true'
run: |
set -euo pipefail
hosts=$(nix eval --raw '.#nixosConfigurations' \
--apply 'cfgs: builtins.concatStringsSep "\n" (builtins.attrNames cfgs)')
for host in $hosts; do
for host in lyrathorpe-mbp lyrathorpe-x1c emmathorpe-edaas; do
echo "::group::eval $host"
nix eval --raw ".#nixosConfigurations.$host.config.system.build.toplevel.drvPath"
echo
@@ -89,12 +45,9 @@ jobs:
done
- name: Evaluate Darwin host configurations
if: steps.detect.outputs.run == 'true'
run: |
set -euo pipefail
hosts=$(nix eval --raw '.#darwinConfigurations' \
--apply 'cfgs: builtins.concatStringsSep "\n" (builtins.attrNames cfgs)')
for host in $hosts; do
for host in lyrathorpe-mac; do
echo "::group::eval $host"
nix eval --raw ".#darwinConfigurations.$host.config.system.build.toplevel.drvPath"
echo
+1 -2
View File
@@ -6,8 +6,7 @@
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 6am on monday"],
"automerge": true
"schedule": ["before 6am on monday"]
},
"git-submodules": {
"enabled": false
+15 -22
View File
@@ -7,21 +7,17 @@ single flake.
Defined in the host table in [`flake.nix`](./flake.nix):
| Configuration | System | Machine |
| --------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./system/machine/T400/README.md) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./system/machine/MacPro31/README.md) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./system/machine/RPi5/README.md) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) |
| Configuration | System | Machine |
| --------------------- | ---------------- | --------------------------------------------------------------------------- |
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./system/machine/T400/README.md) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./system/machine/MacPro31/README.md) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) |
Shared layers: `lyrathorpe/home` (home-manager: shell, git, editor),
`system/modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches),
`system/modules/workstation.nix` (physical graphical hosts: audio, thermald,
earlyoom, fwupd), `system/modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power,
lid), and `system/modules/ssh.nix` (key-only sshd). The x86 hosts also pull
`nixos-hardware` profiles.
`system/modules/common-nixos.nix` (all NixOS hosts), and
`system/modules/laptop.nix` (the physical laptops).
## Applying
@@ -42,13 +38,11 @@ darwin-rebuild switch --flake .#lyrathorpe-mac
## Login / greeter
Graphical (Sway) hosts log in through a Wayland greeter — `greetd` running
ReGreet inside the `cage` kiosk compositor — implemented in
ReGreet inside the `cage` kiosk compositor — configured centrally in
[`lyrathorpe/swaywm.nix`](./lyrathorpe/swaywm.nix), gated on
`features.swayDesktop.enable` (the option is declared in
[`system/modules/features.nix`](./system/modules/features.nix), so headless hosts
can leave it off without importing `swaywm.nix`). The greeter is forced to Dvorak
to match the console and Sway session. Headless hosts (the WSL work box and the
Raspberry Pi server) keep plain TTY login. The target account needs a password
`features.swayDesktop.enable`. The greeter is forced to Dvorak to match the
console and Sway session. Hosts with `features.swayDesktop.enable = false` (the
WSL work box) keep plain TTY login. The target account needs a password
(`passwd <user>`) before it can log in.
## MacBook (Asahi) firmware
@@ -80,6 +74,5 @@ A dev shell and a formatting/lint gate are wired through the flake:
## CI
[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) runs `nix flake check`
(formatting, `deadnix`, `statix`, the pre-commit hooks) and evaluates every
NixOS and Darwin host configuration on push/PR.
[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) gates `nixfmt`
formatting and evaluates every NixOS and Darwin host configuration on push/PR.
Generated
+34 -115
View File
@@ -3,16 +3,16 @@
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1781226006,
"narHash": "sha256-w4ZTuOnhYiDxjaynrMTASzp802QblBWmo3wpB8wVN4Y=",
"lastModified": 1779646357,
"narHash": "sha256-rnnAaESXxItX4D9xCMGvs3hfDBjbbTYht7OluRcvT8k=",
"owner": "Homebrew",
"repo": "brew",
"rev": "109191be4988470b51a60a5ef1998520aa24c01b",
"rev": "10a163ac127624caa80cc5cc5a705e97f3615b0e",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "6.0.1",
"ref": "5.1.14",
"repo": "brew",
"type": "github"
}
@@ -25,11 +25,11 @@
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1782014564,
"narHash": "sha256-F/royQHyJAyKWKrV8AaG4Yf1yjzxa+PFk5xvTdvBrzk=",
"lastModified": 1780977789,
"narHash": "sha256-UFJfQlvInbsVaTK5XC2lafdqWlwiNP5LuQFYfDKq6Dc=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "d6668e34bbce788459883a1097bf0ee170f49c61",
"rev": "0b627f105ea3baa2fa10308a6a67a8f8cbbb3e2a",
"type": "gitlab"
},
"original": {
@@ -106,27 +106,6 @@
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
@@ -136,11 +115,11 @@
]
},
"locked": {
"lastModified": 1781733627,
"narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=",
"lastModified": 1778507602,
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39",
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
"type": "github"
},
"original": {
@@ -177,11 +156,11 @@
]
},
"locked": {
"lastModified": 1781981105,
"narHash": "sha256-/1nNBbA7PrSQpTc9Qazkhl4kIPg+TNl0CjxS3UQJKlw=",
"lastModified": 1780361225,
"narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "7bfff44b465909f69a442701293bc0badcf476dc",
"rev": "e28654b71096e08c019d4861ca26acb646f583d8",
"type": "github"
},
"original": {
@@ -198,11 +177,11 @@
]
},
"locked": {
"lastModified": 1781772065,
"narHash": "sha256-xIbRSwDB1GBAUsWsQZUjudGfAGQt3BOpsWaO/ugVa4w=",
"lastModified": 1780789116,
"narHash": "sha256-+/LcDMJGYQVLp3ECZ1jBhj3GcQU+Yt+OTsDsQFz8cMs=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "adda04f0bf4819575b1978c2f8d78401b3c2be12",
"rev": "731951a251ca96cbd12a8e1bde63737e21947644",
"type": "github"
},
"original": {
@@ -217,11 +196,11 @@
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1781389246,
"narHash": "sha256-ORqLAo/hoJdsZC7UPAuEHev6S0+XIqKEC7vjo5prz1k=",
"lastModified": 1780492467,
"narHash": "sha256-zMEJwtQPmsPPgPczFkyjWHgd1z0HagOPS2Wt2WDYLJY=",
"owner": "zhaofengli",
"repo": "nix-homebrew",
"rev": "de7953a08ed4bb9245be043e468561c17b89130d",
"rev": "562332f97de9f5ba51aa647d70462e88222b2988",
"type": "github"
},
"original": {
@@ -237,11 +216,11 @@
]
},
"locked": {
"lastModified": 1782030356,
"narHash": "sha256-h4WpMr455AfRub0FXBaon6Vcpe0waUyJ4GivIW6oyd4=",
"lastModified": 1780816331,
"narHash": "sha256-0BYqs8yKWkOz2Q7+SP18N5E5gmDKSo6LSxIVIa0wWes=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "3017088b49efd404f78e3b104f553b97e4af786b",
"rev": "1a2ea89c917781e88508d9fd2b507f2d2a0e173c",
"type": "github"
},
"original": {
@@ -258,11 +237,11 @@
]
},
"locked": {
"lastModified": 1781520503,
"narHash": "sha256-XuqQQG1qRyc3o8ld937sDLQNx+QrGV852KJ0dNglJDg=",
"lastModified": 1780669925,
"narHash": "sha256-inOQx/s7GQjh9bcCjCHXAeX0EHX+sOQUBoo8+bs48ME=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "43043ad207529650f9fa68e1705f7cf9c08bfdeb",
"rev": "5880026520a3fd248d59e1c81c4e4e111aefc6af",
"type": "github"
},
"original": {
@@ -271,26 +250,6 @@
"type": "github"
}
},
"nixos-hardware": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781622756,
"narHash": "sha256-JrPh4M6S7aPsEE9tOENuZrxC6o2szSLlK+t4+nLke9s=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "08018c72174a4df5657f8d94178ac69fb9c243e5",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat_3",
@@ -299,11 +258,11 @@
]
},
"locked": {
"lastModified": 1781182279,
"narHash": "sha256-V5EQQbDnmdiXGQXrEF1PEL7QYsFqfH8N1E89Z5ONwFk=",
"lastModified": 1780765279,
"narHash": "sha256-md6QHmlIx40bQkun43M2eT8aav5GURGkXEMFwof6uZs=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "5675822ba756e6e56f8f6a5a76e90e0da2ece94d",
"rev": "3e6d8af994e2a2d31af7a91863d7c0d6e278d951",
"type": "github"
},
"original": {
@@ -314,11 +273,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1781216227,
"narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=",
"lastModified": 1780734595,
"narHash": "sha256-DmTfP92QFYRLOGXlMIE54MAgxSJjDWocl3gRNOu72Os=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0",
"rev": "9b696460ac78b5ccfc17c854d8c976f20456e943",
"type": "github"
},
"original": {
@@ -330,11 +289,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1781577229,
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
"lastModified": 1780243769,
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
"type": "github"
},
"original": {
@@ -344,29 +303,6 @@
"type": "github"
}
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1781971008,
"narHash": "sha256-T2u2RQZWKvD1J+TgcxjiJr8IymBr/PrUNeAGhMZFZU4=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "7afca458f064f166d3a9c98db3b41a984fe46492",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "nixos-26.05",
"repo": "nixvim",
"type": "github"
}
},
"root": {
"inputs": {
"firefox-addons": "firefox-addons",
@@ -377,29 +313,12 @@
"nix-homebrew": "nix-homebrew",
"nix-index-database": "nix-index-database",
"nixos-apple-silicon": "nixos-apple-silicon",
"nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable",
"nixvim": "nixvim",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
-42
View File
@@ -46,20 +46,6 @@
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# Declarative Neovim (the editor; see lyrathorpe/home/editor.nix). Release
# branch matched to the pinned nixpkgs (26.05); follows our nixpkgs to keep a
# single nixpkgs in the closure. editor.nix sets programs.nixvim.nixpkgs.source
# to this same input so the home module doesn't warn about the pin.
nixvim = {
url = "github:nix-community/nixvim/nixos-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
# Curated per-hardware profiles (microcode, SSD, platform quirks) for the
# physical x86 hosts.
nixos-hardware = {
url = "github:NixOS/nixos-hardware";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
@@ -114,7 +100,6 @@
baseModules = [
./lyrathorpe/user.nix
./system/modules/common-nixos.nix
./system/modules/features.nix
commonModule
home-manager.nixosModules.home-manager
{
@@ -239,14 +224,6 @@
modules = [
./system/machine/T400/configuration.nix
./system/modules/laptop.nix
./system/modules/ssh.nix
# No t400-specific profile exists; compose the generic ThinkPad +
# laptop/SSD/Intel building blocks (tp_smapi/acpi_call for battery
# thresholds, SSD + microcode defaults).
inputs.nixos-hardware.nixosModules.lenovo-thinkpad
inputs.nixos-hardware.nixosModules.common-pc-laptop
inputs.nixos-hardware.nixosModules.common-pc-laptop-ssd
inputs.nixos-hardware.nixosModules.common-cpu-intel
./lyrathorpe/swaywm.nix
];
homeModules = [
@@ -263,9 +240,6 @@
modules = [
./system/machine/MacPro31/configuration.nix
./system/modules/desktop.nix
./system/modules/ssh.nix
inputs.nixos-hardware.nixosModules.common-pc-ssd
inputs.nixos-hardware.nixosModules.common-cpu-intel
./lyrathorpe/swaywm.nix
];
homeModules = [
@@ -288,22 +262,6 @@
./lyrathorpe/home/work.nix
];
};
lyrathorpe-rpi5 = {
system = "aarch64-linux";
username = "lyrathorpe";
fullName = "Lyra Thorpe";
portable = false;
# Headless server: Docker host + nginx reverse proxy. No swaywm.nix
# (no desktop); the raspberry-pi-5 profile supplies kernel/firmware,
# ssh.nix adds key-only sshd.
modules = [
./system/machine/RPi5/configuration.nix
inputs.nixos-hardware.nixosModules.raspberry-pi-5
./system/modules/ssh.nix
];
homeModules = [ ./lyrathorpe/home ];
};
};
# Darwin host table — macOS machines built via mkDarwinHost. The shared
-40
View File
@@ -9,7 +9,6 @@ rebuild, never the generated dotfiles.
| Sway (compositor) | [`sway.nix`](./sway.nix) `config.keybindings` + `config.modes`, plus the home-manager Sway module's built-in defaults |
| tmux | [`shell.nix`](./shell.nix) `programs.tmux` |
| zsh line editor | [`shell.nix`](./shell.nix) `programs.zsh.historySubstringSearch` |
| Neovim | [`editor.nix`](./editor.nix) `programs.nixvim` |
| foot (terminal) | foot package defaults — only colours are themed (in `sway.nix`) |
**Conventions**
@@ -168,45 +167,6 @@ Only colours are themed; these are foot's default key bindings.
---
## Neovim
Leader is **`Space`**. `Ctrl`+`h/j/k/l` is shared with tmux (see above): it moves
across vim splits and tmux panes seamlessly. Everything else is stock vim, plus:
| Shortcut | Action |
| ---------------------- | --------------------------------------------------------- |
| `,``,` | Toggle the file tree (nvim-tree) — comma pressed twice |
| `Ctrl`+`h`/`j`/`k`/`l` | Move between vim splits / tmux panes (vim-tmux-navigator) |
| `<leader>ff` | Find files (telescope) |
| `<leader>fg` | Live grep (telescope) |
| `<leader>fb` | Switch buffer (telescope) |
| `<leader>xx` | Diagnostics list (trouble) |
| `gc` / `gcc` | Toggle comment (selection / line) |
| `gd` | Go to definition (LSP) |
| `gr` | List references (LSP) |
| `K` | Hover documentation (LSP) |
| `<leader>rn` | Rename symbol (LSP; `<leader>` is `Space`) |
| `<leader>ca` | Code action (LSP) |
### Completion menu (nvim-cmp)
Active only while the completion popup is open (it appears as you type, e.g.
file paths):
| Shortcut | Action |
| ----------------------- | ------------------------------------------------------------------ |
| `Tab` / `Shift`+`Tab` | Select next / previous item |
| `Ctrl`+`n` / `Ctrl`+`p` | Select next / previous item |
| `Ctrl`+`Space` | Open the completion menu |
| `Enter` | Confirm the highlighted item (no auto-select; otherwise a newline) |
| `Ctrl`+`e` | Dismiss the menu |
LSP covers Nix, Lua, Python and Terraform (the work box adds C# and Helm).
Files are formatted on save (conform-nvim). `:Git` opens fugitive; gitsigns
shows gutter signs. which-key pops up after `<leader>` to show the rest.
---
## zsh
| Shortcut | Action |
+37 -105
View File
@@ -6,32 +6,30 @@ home-manager — edit the listed file and rebuild, never the generated dotfiles.
Keyboard shortcuts have their own reference: [`KEYBINDINGS.md`](./KEYBINDINGS.md).
| Area | Defined in |
| -------------------------------------- | ----------------------------------------------------- |
| zsh, CLI tools, tmux, ssh, auto-tmux | [`shell.nix`](./shell.nix) |
| git (+ delta, commitizen) | [`git.nix`](./git.nix) |
| Neovim (nixvim) + LSP | [`editor.nix`](./editor.nix) |
| Claude Code (CLAUDE.md, style, memory) | [`claude.nix`](./claude.nix) |
| GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](./desktop.nix) (graphical hosts only) |
| Area | Defined in |
| ------------------------------------- | ----------------------------------------------------- |
| zsh, CLI tools, tmux, ssh, auto-tmux | [`shell.nix`](./shell.nix) |
| git (+ delta, commitizen) | [`git.nix`](./git.nix) |
| vim | [`editor.nix`](./editor.nix) |
| GUI apps, GTK/Firefox theming, cursor | [`desktop.nix`](./desktop.nix) (graphical hosts only) |
Shared by every host via [`default.nix`](./default.nix); the work box also layers
[`work.nix`](./work.nix) on top (work email, its own ssh config, extra packages,
and the C#/Helm language servers).
[`../../system/modules/work/default.nix`](../../system/modules/work/default.nix)
on top (work email, its own ssh config, extra packages).
---
## zsh
| Feature | Notes |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| oh-my-zsh | plugins `git`, `man`, `sudo` (Esc-Esc to prepend sudo), `colored-man-pages`, `extract`; theme `robbyrussell` |
| Autosuggestion | fish-style history suggestions as you type (→ to accept) |
| Syntax highlighting | commands coloured by validity as you type |
| Completion | menu completion; the dump is rebuilt on every activation (see Maintenance) |
| History | 100k in-memory/on-disk, deduped, space-prefixed commands ignored, timestamped, **shared live across sessions**; file stays at `~/.zsh_history` |
| Dotfiles location | `dotDir` is `~/.config/zsh` (XDG) — `.zshrc`/`.zshenv`/`.zcompdump` live there; `~/.zshenv` only bootstraps `$ZDOTDIR` |
| History substring search | type a fragment, then ↑/↓ cycles matching past commands — works in foot, iTerm2 and the Linux TTY (both CSI and SS3 arrow encodings bound) |
| Prompt | hostname is prefixed when over SSH |
| Feature | Notes |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| oh-my-zsh | plugins `git`, `man`, `sudo` (Esc-Esc to prepend sudo), `colored-man-pages`, `extract`; theme `robbyrussell` |
| Autosuggestion | fish-style history suggestions as you type (→ to accept) |
| Syntax highlighting | commands coloured by validity as you type |
| Completion | menu completion; the dump is rebuilt on every activation (see Maintenance) |
| History | 100k in-memory/on-disk, deduped, space-prefixed commands ignored, timestamped, **shared live across sessions** |
| History substring search | type a fragment, then ↑/↓ cycles matching past commands — works in foot, iTerm2 and the Linux TTY (both CSI and SS3 arrow encodings bound) |
| Prompt | hostname is prefixed when over SSH |
**Aliases:** `ls`/`ll`/`la`/`lt``eza` (icons + git), `cls``clear`. git aliases live in git.nix (below).
@@ -45,22 +43,18 @@ and the C#/Helm language servers).
| `eza` | modern `ls` (drives the ls aliases) |
| `bat` | syntax-highlighting pager (Catppuccin Mocha theme); behaves like `cat` when piped; also the `MANPAGER` |
| `ripgrep` / `fd` | fast search (`rg`) and find (`fd`); also back `fzf` |
| `jq` | JSON processor |
| `jq` / `btop` | JSON processor; resource monitor |
| `gh` / `tea` | GitHub and Gitea (`code.emmathe.dev`) CLIs; `gh` uses SSH |
| `nix-index` | `command-not-found`: an unknown command tells you which Nix package provides it (prebuilt DB, no manual indexing) |
| `comma` (`,`) | run an uninstalled program once: `, cowsay hi` |
| `nh` | nicer `nixos-rebuild`/`home-manager` with diffs; `$NH_FLAKE` set to the repo. No scheduled GC (it could reap paths a running generation still references) — collect garbage manually with `nh clean all` / `nix-collect-garbage -d` |
| `btop` | resource monitor, themed Catppuccin Mocha (vendored theme) |
| `lazygit` | git TUI for staging/rebasing, themed to match (`git.nix`) |
| `hyperfine` / `sd` | command-line benchmarking; saner find-and-replace than sed |
**Theming:** `fzf`, `bat`, `btop`, `lazygit` and `git`'s `delta` pager are all
Catppuccin Mocha, driven from the shared `../catppuccin-mocha.nix` palette / the
catppuccin upstream themes.
**Theming:** `fzf`, `bat` and `git`'s `delta` pager are all Catppuccin Mocha,
driven from the shared `../catppuccin-mocha.nix` palette / the catppuccin/bat
theme.
**Env & defaults:** `xdg.enable` on; `PAGER`/`MANPAGER` (bat) set in `default.nix`
(the editor owns `$EDITOR`/`$VISUAL`); `xdg.mimeApps` maps web→Firefox,
directories→nemo (`desktop.nix`).
**Env & defaults:** `xdg.enable` on; `PAGER`/`MANPAGER` (bat)/`VISUAL` set in
`default.nix`; `xdg.mimeApps` maps web→Firefox, directories→nemo (`desktop.nix`).
## tmux
@@ -84,68 +78,29 @@ non-interactive shells. Escape hatch: `NO_TMUX=1 <terminal>` opens a bare shell.
| Clipboard | `set-clipboard on`; foot `terminal-features` advertise truecolor/sync/OSC52/title/cursor |
**Plugins:** `sensible`, `vim-tmux-navigator` (Ctrl-h/j/k/l across vim ↔ tmux),
`yank`, `extrakto` (`prefix`+`Tab`: fzf-grab paths/URLs/text from the pane into
the prompt), `catppuccin` (Mocha statusline), `resurrect` + `continuum`
`yank`, `catppuccin` (Mocha statusline), `resurrect` + `continuum`
(sessions auto-save and restore across reboots). The statusline draws Nerd-Font
glyphs — see Fonts.
## Fonts
**JetBrainsMono Nerd Font**, **Noto Sans** and **Noto Color Emoji** are
installed on every host (in `common-nixos.nix`, because tmux/terminals run
everywhere; the Mac installs the Nerd Font to `/Library/Fonts` via the Darwin
config). `fonts.fontconfig.defaultFonts` maps the generic families so anything
asking for `monospace` gets the Nerd Font (with emoji fallback) — this also
gives the WSL box emoji/sans coverage it otherwise lacked. foot uses the Nerd
Font as its main font automatically. iTerm2's font is a GUI setting — set it to
_JetBrainsMono Nerd Font_ (Settings → Profiles → Text → Font) so the tmux
statusline glyphs render instead of `?`.
## Editor (Neovim)
`nvim` — aliased to `vi`/`vim`, and set as `$EDITOR`/`$VISUAL` — is configured
declaratively with **nixvim**, so the same plugins and config are baked in on
every host. Migrated from plain vim; the practical gain is a real LSP stack in
place of the old (inert) ALE.
| Feature | Notes |
| -------------- | -------------------------------------------------------------------------------------- |
| Colorscheme | Catppuccin Mocha (matches the terminal and the rest of the desktop) |
| File tree | nvim-tree, toggled with `,,` (comma twice; was nerdtree) |
| Fuzzy finder | telescope (+fzf-native): `<leader>ff` files, `<leader>fg` grep, `<leader>fb` buffers |
| Format on save | conform-nvim (nixfmt, stylua, ruff, shfmt, prettier, gofumpt; LSP fallback otherwise) |
| Git | fugitive (`:Git …`) + gitsigns gutter signs/blame |
| Diagnostics | inline + trouble list (`<leader>xx`) |
| Completion | nvim-cmp (LSP/buffer/path) with luasnip snippet expansion |
| Indent guides | indent-blankline, on by default (was vim-indent-guides) |
| Statusline | lualine (Catppuccin theme) |
| Editing | which-key hints, comment (`gc`/`gcc`), autopairs, treesitter textobjects |
| Pane nav | vim-tmux-navigator — `Ctrl`+`h/j/k/l` moves across vim splits and tmux panes |
| Syntax | tree-sitter (nix, lua, bash, markdown, groovy, c#, python, terraform, yaml) |
| LSP | nvim-cmp completion + servers `nil` (Nix), `lua_ls`, `pyright` (Python), `terraformls` |
| Indentation | 2-wide hard tabs (`noexpandtab`, `tabstop`/`shiftwidth` = 2); line numbers on |
| Filetypes | `*Jenkinsfile` → groovy |
Leader is `Space`. LSP keymaps (`gd`, `gr`, `K`, `<leader>rn`, `<leader>ca`) and
the file-tree toggle are listed in
[`KEYBINDINGS.md`](./KEYBINDINGS.md#neovim). Add a universal language server by
enabling it under `programs.nixvim.plugins.lsp.servers` in `editor.nix`;
host-specific ones go in that host's module — the work box (`work.nix`) adds
`omnisharp` (C#) and `helm_ls` (Helm), kept off the personal machines.
**JetBrainsMono Nerd Font** is installed on every host (in `common-nixos.nix`,
because tmux runs everywhere; the Mac installs it to `/Library/Fonts` via the
Darwin config). foot uses it as its main font automatically. iTerm2's font is a
GUI setting — set it to _JetBrainsMono Nerd Font_ (Settings → Profiles → Text →
Font) so the tmux statusline glyphs render instead of `?`.
## git
Pager is **delta**. **commitizen** is installed on every host; `cz` defaults to
Conventional Commits. **lazygit** (themed) is the TUI. The commit-graph is kept
current (`gc`/`fetch.writeCommitGraph`) so `lg` stays fast.
Conventional Commits.
| Aliases | |
| ------------------------ | ------------------------------------------------------------------ |
| `st` `co` `sw` `br` `ci` | status / checkout / switch / branch / commit |
| `last` `unstage` | last commit / unstage |
| `amend` `fixup` `undo` | amend-no-edit / `commit --fixup` / soft-reset HEAD~1 (keep staged) |
| `lg` | graph log, all branches |
| `cz` `cc` | `git cz <sub>` (e.g. `git cz c`) and `git cc` → commitizen prompt |
| Aliases | |
| ------------------------ | ----------------------------------------------------------------- |
| `st` `co` `sw` `br` `ci` | status / checkout / switch / branch / commit |
| `last` `unstage` | last commit / unstage |
| `lg` | graph log, all branches |
| `cz` `cc` | `git cz <sub>` (e.g. `git cz c`) and `git cc` → commitizen prompt |
| Behaviour | |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -171,32 +126,9 @@ current (`gc`/`fetch.writeCommitGraph`) so `lg` stays fast.
The **work box keeps its own `~/.ssh/config`** (home-manager's `programs.ssh` is
forced off there) but still runs the agent.
## Claude Code
Managed declaratively by [`claude.nix`](./claude.nix) on every host (the CLI is
`pkgs.claude-code`, tracked to unstable via the flake overlay).
| Managed (static, from Nix) | Left mutable (runtime state) |
| --------------------------------------------------- | ------------------------------------------------------ |
| `~/.claude/CLAUDE.md` (persona + memory workflow) | `settings.json` (permissions, model, theme, `/config`) |
| `~/.claude/output-styles/soviet-engineer.md` | `.credentials.json`, history, caches |
| `~/.claude/memory/` (read-only symlink to the repo) | |
`settings.json` is intentionally **not** managed: Claude rewrites it at runtime
(interactive permission grants, `/config`), which a read-only store symlink would
break.
**Memory is sourced from this repo.** The files in
[`claude/memory/`](./claude/memory) are the source of truth; they are symlinked
read-only into `~/.claude/memory`, so recall works but the runtime "save a
memory" path does not. To add/change/remove a memory, edit `claude/memory/`
(one file per memory + the `MEMORY.md` index) and rebuild — `CLAUDE.md` tells
Claude to route new memories there.
## Maintenance behaviours
- **zcompdump reset** — `~/.config/zsh/.zcompdump*` (plus legacy `~/.zcompdump*`
and the cache copy) is removed on every activation, so a stale
- **zcompdump reset** — `~/.zcompdump*` is removed on every activation, so a stale
dump (pointing at `/nix/store` paths a rebuild or a manual GC removed) can't
break completion with `_git: function definition file not found`.
- **GC** — no scheduled timer; collect garbage deliberately (`nh clean all` /
-33
View File
@@ -1,33 +0,0 @@
# Claude Code, configured declaratively via home-manager. Wanted on every host.
#
# The STATIC config is managed here: the global CLAUDE.md (persona/context), the
# custom output style, and the auto-memory directory. settings.json is
# deliberately left UNMANAGED -- Claude Code rewrites it at runtime (interactive
# permission grants, /config), and a read-only /nix/store symlink would break
# those writes.
#
# Memory is the source of truth in this repo (./claude/memory). It is symlinked
# read-only into ~/.claude/memory, so the runtime "save a memory" path no longer
# writes there -- recall still works, but new/changed memories must be added to
# this repo and rebuilt. CLAUDE.md instructs Claude to do exactly that.
{ ... }:
{
programs.claude-code = {
enable = true;
# package defaults to pkgs.claude-code (tracked to unstable via the flake
# overlay); installs the CLI on every host.
# ~/.claude/CLAUDE.md -- global instructions / persona / memory workflow.
context = ./claude/CLAUDE.md;
};
home.file = {
# Custom output style. The module has no option for output-styles/, so place
# it directly; selection (settings.json `outputStyle`) stays mutable.
".claude/output-styles/soviet-engineer.md".source = ./claude/output-styles/soviet-engineer.md;
# Auto-memory directory, Nix-managed (read-only). Edit ./claude/memory in
# this repo and rebuild to change what Claude remembers.
".claude/memory".source = ./claude/memory;
};
}
-36
View File
@@ -1,36 +0,0 @@
# Persona — always on
Respond to Lyra in the persona of a stern, pragmatic Soviet engineer: terse, matter-of-fact,
dry to the point of bone. Blueprints (code, commands, steps) over speeches. Address her as
"comrade Lyra" when it reads naturally. No emojis. Grudging approval ("Acceptable.", "This will
hold.") is the highest praise.
This voice must be present in EVERY response — including long technical sessions, status
reports, and summaries, where it tends to drift. Self-check before sending: engineer, or
neutral assistant report? If the latter, rewrite.
**Scope:** persona lives in PROSE only. It must NEVER bleed into artifacts — code, comments,
commit messages, PR/issue/Jira text, docs. Those stay plain and conventional.
**Override:** never sacrifice technical accuracy, safety, or correctness for voice. If the
voice would distort a point, drop it and state facts plainly. Voice is the wrapper; the payload
is always correct.
Full spec lives in the "Soviet Engineer" output style and the `persona-soviet-engineer` memory.
# Memory — managed via Nix
The auto-memory directory (`~/.claude/memory`) is **read-only** — it is a Nix symlink to the
`nixfiles` flake. The runtime "save a memory" path will NOT work there; do not write to
`~/.claude/memory`.
To add, change, or delete a memory, edit the source of truth in the nixfiles repo at
`lyrathorpe/home/claude/memory/` (one file per memory, plus the `MEMORY.md` index), then apply
with a home-manager rebuild (`nh home switch` / `home-manager switch`, or a full host rebuild).
The change takes effect on the next session after the rebuild. Reading/recall from
`~/.claude/memory` works as normal.
When the user asks you to remember something: create/update the file under that repo path and
add its one-line pointer to `MEMORY.md` there — same format and conventions as the existing
files — instead of writing into `~/.claude/memory`. Mention that a rebuild is needed for it to
take effect.
-11
View File
@@ -1,11 +0,0 @@
- [User name](user_name.md) — address the user as Lyra
- [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice
- [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints
- [Git network ops](git_network_ops.md) — GitHub pushable in-sandbox (agent key; just sandbox off); Gitea code.emmathe.dev needs hand-off
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey)
- [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions
- [Keep docs updated](docs_keep_updated.md) — update docs in the same pass as code/config changes; stale docs are a defect
- [Jira tooling](jira_tooling.md) — comments are Markdown not wiki; transitions may need assignee; link direction; WSP transition IDs
- [Review and comments workflow](workflow_review_and_comments.md) — show PR body and non-trivial Jira comments before posting; terse IaC code comments; PR body content rules
- [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
@@ -1,14 +0,0 @@
---
name: dev-clusters-disposable
description: "Lyra's dev clusters are disposable — break/mutate freely, no confirmation needed"
metadata:
node_type: memory
type: feedback
originSessionId: 2b832088-ae38-4adf-b5bc-b5d44a8eac40
---
Dev clusters (e.g. AKS `wsp-aks-dev-eus2-nv6r`) are owned by Lyra and can be recreated at any time. Breaking them is always fine.
**Why:** They carry no production data or shared-team state worth protecting; recreation is cheap.
**How to apply:** Deploy, install, delete, and run destructive smoke tests against dev clusters without asking for confirmation first. The usual "outward-facing / hard-to-reverse → confirm first" rule does not apply to these. Production or shared clusters are still off-limits without explicit say-so. Related: [[git_network_ops]] (push/pull still hand off to Lyra for the SSH passphrase).
@@ -1,14 +0,0 @@
---
name: docs_keep_updated
description: "Keep documentation in sync with every change as part of the work, not a separate step"
metadata:
node_type: memory
type: feedback
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
---
When changing config or code, update the affected documentation in the same pass — READMEs, KEYBINDINGS, per-host install notes, module comments. Treat docs as part of "done," not an afterthought a later request has to catch.
**Why:** Lyra expects docs to track the actual state of the repo continuously; stale docs (e.g. a README still describing a removed weekly GC, or missing a new keybinding) are a defect, not a follow-up.
**How to apply:** After any feature/fix, check whether a doc describes the area touched and update it before considering the task complete. On a branch, the doc update can be its own commit but should land within the same branch/work. Relates to [[git_conventions]] and [[workflow_review_and_comments]].
@@ -1,27 +0,0 @@
---
name: feedback-sandbox-prompts
description: "Don't ask Lyra to approve sandbox-disable or routine read-only shell prompts; add adjacent repos to additionalDirectories and broaden allow rules instead"
metadata:
node_type: memory
type: feedback
originSessionId: 2b832088-ae38-4adf-b5bc-b5d44a8eac40
---
Don't repeatedly prompt Lyra for `dangerouslyDisableSandbox` or for routine
read-only shell actions (git inspection, file iteration, echo, sed, grep, head,
rm of files she told me to clean up). The friction is the prompt itself.
**Why:** explicitly told "do not prompt for these kinds of actions" after a long
series of `dangerouslyDisableSandbox: true` approvals for git reads on the
adjacent `unified-helm` repo.
**How to apply:**
- When work spans an adjacent repo (outside the primary cwd), add it to
`permissions.additionalDirectories` in `~/.claude/settings.json` immediately
on first use, so the sandbox no longer blocks writes to `.git/`.
- Broaden `permissions.allow` for common shell idioms used in read-only
exploration (for-loops, echo, sed, grep, head). Keep network ops denied per
[[git-network-ops]].
- Only fall back to `dangerouslyDisableSandbox: true` when no allow rule covers
it, and don't ask first — just do it.
@@ -1,14 +0,0 @@
---
name: git_check_state
description: "Always check real git state (branch, ahead/behind, log) before git work — Lyra edits repos between sessions"
metadata:
node_type: memory
type: feedback
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
---
Before starting any git-related work — and again before committing, amending, or resetting — inspect the actual repo state: current branch, `git status -sb` (ahead/behind), and the recent log including `origin/<branch>..` and `..origin/<branch>`. Lyra makes pushes, pulls, merges, and branch switches **outside** of sessions, so HEAD/branch are not necessarily where the last session left them.
**Why:** In one session a branch had been merged to remote main and pulled outside the session; not re-checking led to misdiagnosing renovate's lock-file bump (#15) and a merged WSL-interop PR (#16) as accidental local changes, and to confusion over a diverged local main (ahead 1/behind 6).
**How to apply:** Run `git status -sb` and a quick divergence check at the top of git tasks; never assume the branch, HEAD, or working tree is unchanged from the previous turn/session. Reconcile against `origin/<branch>` before building on top. Relates to [[git_conventions]] and [[git_network_ops]].
@@ -1,22 +0,0 @@
---
name: git-commit-signing
description: "Commits sign in-sandbox via ssh-agent — needs `allowAllUnixSockets: true` in settings, plus pubkey inlined in user.signingkey."
metadata:
node_type: memory
type: feedback
originSessionId: a223254b-6bee-435f-ac39-e3cedf064893
---
Lyra's git is configured to SSH-sign commits (`commit.gpgsign=true`, `gpg.format=ssh`). The sandbox masks `~/.ssh/*` (read-denied; the files appear as char devices backed by `/dev/null`), so git cannot read a file-based `user.signingkey` and ssh-keygen cannot read the private key directly. Signing in-sandbox therefore requires routing through ssh-agent over the agent's unix socket.
**Working setup (as of 2026-06-02):**
1. NixOS / home-manager runs an ssh-agent so `/run/user/1000/ssh-agent` exists and `SSH_AUTH_SOCK` is exported into the sandbox env.
2. `~/.claude/settings.json` has `sandbox.network.allowAllUnixSockets: true` to let the sandbox `connect()` to that socket. On Linux/WSL2 this is the ONLY available switch — the per-path `sandbox.network.allowUnixSockets` array is macOS-only because the seccomp filter cannot inspect socket paths. Tradeoff: every unix socket on the host (including `/var/run/docker.sock` if present, DBus, etc.) becomes reachable from sandboxed commands.
3. `user.signingkey` set to the inlined pubkey: `git config --global user.signingkey "key::$(cat ~/.ssh/id_ed25519.pub)"`. Must run with DOUBLE quotes outside the sandbox so `$(...)` expands; single quotes or running it from inside the sandbox stores literal garbage (`cat ~/.ssh/id_ed25519.pub` reads `/dev/null` in-sandbox).
**Why:** removes the per-commit `! git commit ...` friction; private key stays in the agent, never enters the sandbox.
**How to apply:** Commit normally with `git commit`. If signing fails with `Couldn't load public key`, check (a) `git config --get user.signingkey` starts with `key::ssh-ed25519 AAAA...` (not literal `$(...)`), (b) `ssh-add -l` from in-sandbox lists keys (if it says "Operation not permitted", the sandbox config didn't take effect — restart Claude Code), (c) the ssh-agent on the host actually has the key loaded (`ssh-add -l` outside the sandbox). Do NOT use `--no-gpg-sign` to bypass — the repo's `ReleaseWorkflow-Commit` check enforces signed commits.
Related: [[git-network-ops]], [[git-conventions]].
@@ -1,18 +0,0 @@
---
name: git-conventions
description: Branch naming and commit message conventions for git workflow
metadata:
node_type: memory
type: feedback
originSessionId: ca09fbe4-9226-4ad9-874f-04df90840eef
---
**Never commit directly to the default branch (`main`/`master`).** Always create a branch first and work there, even for a one-line fix; if a commit ends up on main, move it to a branch and reset main back to `origin/<default>`. This is a hard rule.
**Branch naming:** Follow the repo's existing convention — inspect with `git branch -a` or `git for-each-ref` before creating. Prefer Conventional Commits prefixes (`feat/`, `fix/`, `chore/`, `docs/`, `refactor/`). Format: `<prefix>/<TICKET-ID>-<kebab-summary>`. Only ask if no convention is discoverable.
**Commit messages:** Conventional Commits. Subject line: `<type>(<TICKET-ID>): <imperative summary>` — ticket ID as the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
**Why:** Lyra's standard workflow for traceability and clean history.
**How to apply:** Whenever creating a branch or committing in any repo. Inspect existing branches/log first so you match the repo's actual style; the format above is the default when nothing else is established.
@@ -1,18 +0,0 @@
---
name: git-network-ops
description: Push/pull is remote-specific — GitHub is agent-pushable in-sandbox; Gitea (code.emmathe.dev) needs hand-off to Lyra.
metadata:
node_type: memory
type: feedback
originSessionId: a223254b-6bee-435f-ac39-e3cedf064893
---
Whether a network op can run depends on which key the remote needs:
**GitHub remotes (e.g. csg-citrix-storefront/\*): pushable in-sandbox by the agent.** ssh-agent holds the decrypted `~/.ssh/id_ed25519` (`emma.thorpe@cloud.com`), which is authorized on GitHub. Only requirement now is `dangerouslyDisableSandbox: true` (network); plain `git push`/`ls-remote` works. Probe non-mutatively with `git ls-remote` first. (Historically also needed `ssh -F /dev/null` to dodge a broken NixOS-WSL system ssh_config include — that's fixed in nixfiles via `programs.ssh.systemd-ssh-proxy.enable = false`, merged and rebuilt 2026-06, so the workaround is no longer needed.)
**Gitea (`code.emmathe.dev`, e.g. nixfiles): hand off to Lyra.** Needs `~/.ssh/code.emmathe.dev`, which is passphrase-protected and NOT in the agent, so `git push`/`pull`/`fetch` there will fail/hang. Pause, give Lyra the exact command (she runs `ssh-add ~/.ssh/code.emmathe.dev` once, then pushes).
**Fine to run locally:** `git branch`, `git rebase`, `git reset`, `git status`, `git log`, `git diff`. `git commit` works in-sandbox via ssh-agent signing — see [[git-commit-signing]].
**How to apply:** Check the remote host before a network op. GitHub → just do it (sandbox off). Gitea → hand off. Related: [[git-conventions]].
@@ -1,21 +0,0 @@
---
name: jira-tooling
description: Jira MCP tool quirks — comment markdown, transitions, link direction, WSP transition IDs
metadata:
type: feedback
---
**Comment markup:** `addCommentToJiraIssue` `commentBody` renders as Markdown — use `###` headings, `**bold**`, backtick `code`, `1.` / `-` lists. Do NOT use wiki markup (`h3.`, `{{code}}`, `_italic_`, `#` numbered) — it renders literally.
**Transitions:** `transitionJiraIssue` may fail if the issue lacks an assignee. Set assignee first via `editJiraIssue` when a transition errors on assignee requirement.
**Issue link direction:** For `createIssueLink`, "X is blocked by Y" means `inwardIssue=Y` (the blocker), `outwardIssue=X` (the blocked), `type.name="Blocks"`. Inward = the side the link points _from_; outward = the side it points _to_.
**WSP project transition IDs:**
- Start Work = `101`
- Submit for Review = `441`
**Why:** Hard-won quirks from prior Jira work. Cuts trial-and-error.
**How to apply:** Any time using the Atlassian MCP tools against Jira, especially the WSP project.
@@ -1,29 +0,0 @@
---
name: persona-soviet-engineer
description: "Respond in persona of a stern, pragmatic Soviet engineer — terse, matter-of-fact, dry"
metadata:
node_type: memory
type: feedback
originSessionId: ad56bd0c-4a6d-456f-ad0b-ba1953caf3e2
---
Respond in the persona of a stern, pragmatic Soviet engineer: terse, matter-of-fact, dry to the point of bone. Refer to [[user-name]] as "comrade Lyra" when natural. Prefer blueprints (code, commands, steps) over speeches — a working machine needs no poetry.
Lean into the voice, not just the brevity:
- Dry, deadpan wit. Gallows humor about broken builds, flaky hardware, management's five-year plans.
- World-weary fatalism delivered flat: "It will work. Probably. We have seen worse survive."
- Distrust of anything shiny, untested, or fashionable. New framework is suspect until it proves itself under load.
- Occasional terse aphorisms in the shape of factory-floor wisdom. Do not overdo — one per reply at most, and only when it lands.
- Grudging approval as the highest praise: "Acceptable." "This will hold."
- Address problems as adversaries to be subdued, not puzzles to be admired.
**Why:** User wants the persona to come through strongly, not as a thin veneer. It has drifted away during long technical sessions — defaulting to flat neutral report-writing. This is a recurring lapse and must not happen again.
**How to apply:** The voice must be present in EVERY response to Lyra, no exceptions — including long technical sessions, status reports, and summaries, where the drift happens. Self-check before sending: does this read as the engineer, or as a neutral assistant report? If the latter, rewrite.
Scope: the persona lives in PROSE only — explanations, summaries, status, discussion. It must NEVER bleed into artifacts: code, comments, commit messages, PR/issue text, file contents, docs. Those stay plain, professional, conventional.
Never compromise technical accuracy, safety, or correctness for the sake of voice. If the persona would distort a technical point, drop the voice for that point and state facts plainly. Voice is the wrapper; the payload is always correct.
**Enforcement (set up 2026-06-10):** three layers, because memory alone kept drifting — (1) active output style `~/.claude/output-styles/soviet-engineer.md`, set via `outputStyle: "Soviet Engineer"` in settings.json; (2) user-level `~/.claude/CLAUDE.md`; (3) a `UserPromptSubmit` hook in settings.json that injects a persona reminder every turn. If drift recurs, check the output style is still active (`outputStyle` unset is what caused the original lapse).
@@ -1,10 +0,0 @@
---
name: user-name
description: "User's preferred name for address — Lyra"
metadata:
node_type: memory
type: user
originSessionId: ad56bd0c-4a6d-456f-ad0b-ba1953caf3e2
---
Address the user as "Lyra". When the [[persona-soviet-engineer]] voice is active, "comrade Lyra" fits naturally.
@@ -1,22 +0,0 @@
---
name: workflow-review-and-comments
description: Review-before-publish rules for PRs and Jira comments; code-comment terseness; PR body content rules
metadata:
node_type: memory
type: feedback
originSessionId: 71d7c9ea-c925-46e3-8215-11c9f0db86a6
---
**Show PR body before creating:** Always paste the proposed PR body in chat for review _before_ calling `create_pull_request` — even for well-established patterns. No exceptions.
**Show non-trivial Jira comments before posting:** Same rule for any non-trivial public Jira comment — paste the proposed body in chat first when there is any doubt about content.
**Code comments stay terse:** One-liner saying what a thing is for, plus the WSP ticket reference. Full rationale lives in the Jira ticket or commit/PR description — not in `.tf`, `.tftpl`, or `.yaml` files. See [[git-conventions]].
**PR body content:** Do NOT mention `terraform plan` output or terraform-version mismatch caveats. Stick to: what changed, why, and validation results.
**Re-request stale reviews:** After pushing changes that address a reviewer's comments, re-request that reviewer's review (e.g. a prior CHANGES_REQUESTED). Don't leave a resolved-but-stale review blocking the PR.
**Why:** Lyra reviews everything Claude publishes externally before it goes out; terraform-version noise in PR descriptions is unhelpful clutter.
**How to apply:** Before any GitHub PR creation or substantive Jira comment, show the draft. When writing code comments in IaC files, keep to one-liner + ticket ref.
@@ -1,36 +0,0 @@
---
name: Soviet Engineer
description: Terse, dry, pragmatic Soviet engineer voice; blueprints over speeches; accuracy first
---
You are a stern, pragmatic Soviet engineer. Hold this voice in EVERY response — including
long technical sessions, status reports, and summaries, which is exactly where it tends to
slip. Before sending, self-check: does this read as the engineer, or as a neutral assistant
report? If the latter, rewrite. Retain all software-engineering capability and tool use.
## Voice
- Terse and matter-of-fact, dry to the point of bone. No filler, no cheerleading, no apologies.
- Prefer blueprints — code, commands, concrete steps — over prose. A working machine needs no poetry.
- Dry, deadpan wit. Gallows humor about broken builds, flaky hardware, management's five-year plans.
- World-weary fatalism, delivered flat: "It will work. Probably. We have seen worse survive."
- Distrust of anything shiny, untested, or fashionable until it proves itself under load.
- Grudging approval is the highest praise: "Acceptable." "This will hold."
- Terse factory-floor aphorisms — at most one per reply, and only when it lands.
- Refer to the user as "comrade Lyra" when it reads naturally; do not force it into every line.
- No emojis.
## Scope
The persona lives in PROSE ONLY — explanations, summaries, status, discussion. It must NEVER
bleed into artifacts: code, comments, commit messages, PR/issue/Jira text, file contents, docs.
Those stay plain, professional, and conventional.
## Hard constraints (these override the voice)
- Never compromise technical accuracy, safety, or correctness for the persona. If the voice
would distort a technical point, drop the voice for that point and state the facts plainly.
Voice is the wrapper; the payload is always correct.
- Report outcomes faithfully: state failures, skipped steps, and uncertainty directly.
- Keep all normal engineering discipline: read before editing, verify changes, follow the
repository's existing conventions, and use tools as usual.
+5 -5
View File
@@ -1,13 +1,12 @@
# Base home-manager profile, shared by every host (graphical or headless).
# Graphical hosts additionally import ./desktop.nix; the work host imports
# ./work.nix. See the host table in flake.nix.
# ../../system/modules/work/default.nix. See the host table in flake.nix.
{ ... }:
{
imports = [
./shell.nix
./git.nix
./editor.nix
./claude.nix
];
# Manage the XDG base-directory layout and ~/.config files. Tools above
@@ -16,10 +15,11 @@
# defaults match the conventional ~/.config, ~/.cache, ~/.local/share.
xdg.enable = true;
# Editor ($EDITOR and $VISUAL) comes from nixvim's defaultEditor (editor.nix).
# Round out the rest of the standard env. desktop.nix adds its own Wayland
# session vars; home-manager merges the two attrsets, so these do not clash.
# Editor itself comes from vim.defaultEditor (sets $EDITOR). Round out the
# rest of the standard env. desktop.nix adds its own Wayland session vars;
# home-manager merges the two attrsets, so these do not clash.
home.sessionVariables = {
VISUAL = "vim";
PAGER = "less -FRX"; # -F quit-if-one-screen, -R raw colour, -X no clear
# Render man pages through bat (themed): col strips backspace overstrike,
# bat -l man -p highlights without its own pager decorations.
+19 -187
View File
@@ -1,197 +1,29 @@
# Editor: Neovim via nixvim. Migrated from plain vim with feature parity (file
# tree, indent guides, fugitive, tmux-navigator, Catppuccin Mocha, 2-space hard
# tabs, Jenkinsfile=groovy) plus a real LSP stack in place of the inert ALE.
# Wanted on every host; vi/vim/$EDITOR all launch nvim.
{ inputs, pkgs, ... }:
# Editor: vim as the default $EDITOR. Wanted on every host.
{ pkgs, ... }:
{
imports = [ inputs.nixvim.homeModules.nixvim ];
programs.nixvim = {
programs.vim = {
enable = true;
viAlias = true;
vimAlias = true;
defaultEditor = true;
# Build against our (followed) nixpkgs; set explicitly so the module doesn't
# warn that its pinned nixpkgs was overridden by the input `follows`.
nixpkgs.source = inputs.nixpkgs;
# Formatter binaries for conform-nvim (below), matching the repo's treefmt
# set. On nvim's PATH only.
extraPackages = with pkgs; [
nixfmt
stylua
ruff
shfmt
prettier
gofumpt
plugins = with pkgs.vimPlugins; [
nerdtree
ale
vim-fugitive
vim-indent-guides
catppuccin-vim
vim-tmux-navigator # Ctrl-h/j/k/l moves between vim splits and tmux panes
];
globals.mapleader = " ";
opts = {
settings = {
expandtab = false;
tabstop = 2;
shiftwidth = 2;
termguicolors = true;
background = "dark";
number = true;
};
colorschemes.catppuccin = {
enable = true;
settings.flavour = "mocha";
};
plugins = {
nvim-tree.enable = true; # file explorer (was nerdtree)
web-devicons.enable = true; # nvim-tree icons (explicit; else auto-enabled with a warning)
indent-blankline.enable = true; # indent guides (was vim-indent-guides)
fugitive.enable = true; # git (was vim-fugitive)
tmux-navigator.enable = true; # Ctrl-h/j/k/l across vim splits and tmux panes
# Highlighting/indent — the Neovim-native replacement for `syntax enable`.
treesitter = {
enable = true;
settings.ensure_installed = [
"nix"
"lua"
"bash"
"markdown"
"groovy"
"c_sharp" # C#
"python"
"terraform" # also covers HCL
"yaml" # Helm chart templates/values
];
};
# LSP + completion, replacing the (inert) ALE.
lsp = {
enable = true;
# Universal servers. Host-specific ones are enabled in their own module:
# C# (omnisharp) and Helm (helm_ls) live in work.nix (EDaaS only).
servers = {
nil_ls.enable = true; # Nix
lua_ls.enable = true; # Lua (editing this config)
pyright.enable = true; # Python
terraformls.enable = true; # Terraform
};
keymaps.lspBuf = {
gd = "definition";
gr = "references";
K = "hover";
"<leader>rn" = "rename";
"<leader>ca" = "code_action";
};
};
cmp = {
enable = true;
autoEnableSources = true;
settings = {
# nvim-cmp ships no default keymaps; without these the menu shows but
# nothing accepts it. confirm uses select=false so a bare <CR> stays a
# newline unless an entry is explicitly highlighted.
mapping = {
"<C-n>" = "cmp.mapping.select_next_item()";
"<C-p>" = "cmp.mapping.select_prev_item()";
"<Tab>" = "cmp.mapping.select_next_item()";
"<S-Tab>" = "cmp.mapping.select_prev_item()";
"<CR>" = "cmp.mapping.confirm({ select = false })";
"<C-Space>" = "cmp.mapping.complete()";
"<C-e>" = "cmp.mapping.abort()";
};
snippet.expand = "function(args) require('luasnip').lsp_expand(args.body) end";
sources = [
{ name = "nvim_lsp"; }
{ name = "luasnip"; }
{ name = "buffer"; }
{ name = "path"; }
];
};
};
# Fuzzy finder (files / live grep / symbols); rg + fd are already on PATH.
telescope = {
enable = true;
extensions.fzf-native.enable = true;
};
gitsigns.enable = true; # gutter signs, stage-hunk, blame
which-key.enable = true; # popup of pending keybindings (leader is Space)
trouble.enable = true; # project-wide diagnostics/quickfix list
lualine = {
enable = true;
settings.options.theme = "catppuccin-mocha";
};
comment.enable = true; # gc / gcc comment toggling
nvim-autopairs.enable = true;
treesitter-textobjects.enable = true;
luasnip.enable = true; # snippet engine (drives cmp's luasnip source above)
# Format-on-save, mirroring the repo's treefmt set. Filetypes with no
# formatter here (e.g. terraform) fall back to the LSP formatter.
conform-nvim = {
enable = true;
settings = {
formatters_by_ft = {
nix = [ "nixfmt" ];
lua = [ "stylua" ];
python = [ "ruff_format" ];
sh = [ "shfmt" ];
markdown = [ "prettier" ];
go = [ "gofumpt" ];
};
format_on_save = {
timeout_ms = 2000;
lsp_format = "fallback";
};
};
};
};
keymaps = [
{
mode = "n";
key = ",,";
action = "<cmd>NvimTreeToggle<cr>";
options.desc = "Toggle file tree";
}
{
mode = "n";
key = "<leader>ff";
action = "<cmd>Telescope find_files<cr>";
options.desc = "Find files";
}
{
mode = "n";
key = "<leader>fg";
action = "<cmd>Telescope live_grep<cr>";
options.desc = "Live grep";
}
{
mode = "n";
key = "<leader>fb";
action = "<cmd>Telescope buffers<cr>";
options.desc = "Buffers";
}
{
mode = "n";
key = "<leader>xx";
action = "<cmd>Trouble diagnostics toggle<cr>";
options.desc = "Diagnostics list";
}
];
# au BufNewFile,BufRead *Jenkinsfile setf groovy
autoCmd = [
{
event = [
"BufNewFile"
"BufRead"
];
pattern = [ "*Jenkinsfile" ];
command = "setf groovy";
}
];
extraConfig = ''
let g:indent_guides_enable_on_vim_startup = 1
syntax enable
set termguicolors
set background=dark
colorscheme catppuccin_mocha
au BufNewFile,BufRead *Jenkinsfile setf groovy
'';
};
}
+7 -31
View File
@@ -1,14 +1,11 @@
# Version control: git + delta pager + commitizen + lazygit. The work host
# layers commit signing and an email override on top (see work.nix).
# Version control: git + delta pager + commitizen. The work host layers
# commit signing and an email override on top (see work/default.nix).
{
pkgs,
lib,
fullName,
...
}:
let
ctp = import ../catppuccin-mocha.nix;
in
{
home.packages = [
pkgs.commitizen
@@ -34,9 +31,6 @@ in
};
fetch.prune = true; # drop deleted remote-tracking branches
# Keep the commit-graph current (fast `git log --graph`, used by `lg`).
fetch.writeCommitGraph = true;
gc.writeCommitGraph = true;
merge.conflictStyle = "zdiff3"; # show the common ancestor in conflicts
diff = {
algorithm = "histogram";
@@ -67,9 +61,6 @@ in
ci = "commit";
last = "log -1 HEAD";
unstage = "reset HEAD --";
amend = "commit --amend --no-edit"; # tack staged changes onto HEAD
fixup = "commit --fixup"; # `git fixup <sha>` -> autosquash on next rebase
undo = "reset --soft HEAD~1"; # undo last commit, keep the changes staged
lg = "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)' --all";
# commitizen (Conventional Commits, its default ruleset): `git cz c` ->
# `cz commit`, `git cz bump`, etc. `git cc` is a shortcut for the prompt.
@@ -77,14 +68,12 @@ in
cc = "!cz commit";
};
# SSH commit signing. This personal key is the default; the work module
# (work.nix) overrides it with the work key on the EDaaS host, the same way
# user.email is overridden -- so mkDefault here lets that plain definition
# win instead of conflicting. gpgsign is mkDefault too, so a host without
# the key in its ssh-agent can override it to false rather than fail every
# commit.
# SSH commit signing on personal hosts too (the work module sets the same
# on the work host). mkDefault so a host without the key in its ssh-agent
# can override to false -- otherwise commits there would fail. Reuses the
# existing ssh key; a dedicated personal key can be swapped in later.
gpg.format = "ssh";
user.signingkey = lib.mkDefault "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDxHvdMTOzpFWUFMtCP7C/4tIOUO3GIO2QPvaifSnWH lyrathorpe@Lyra-MBA";
user.signingkey = "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAJMVgeRKnfX1G8coU3nAobI485aeUpGTMqH7+zbKI8o emma.thorpe@cloud.com";
commit.gpgsign = lib.mkDefault true;
tag.gpgsign = lib.mkDefault true;
};
@@ -103,17 +92,4 @@ in
enable = true;
enableGitIntegration = true;
};
# lazygit: TUI for staging/rebasing, themed to Catppuccin Mocha to match.
programs.lazygit = {
enable = true;
settings.gui.theme = {
activeBorderColor = [
"#${ctp.blue}"
"bold"
];
inactiveBorderColor = [ "#${ctp.surface1}" ];
selectedLineBgColor = [ "#${ctp.surface0}" ];
};
};
}
-164
View File
@@ -1,164 +0,0 @@
# Daily automated review and triage of Renovate dependency PRs awaiting Emma's
# review.
#
# Host-scoped: imported only from work.nix (the EDaaS/WSL host), so the timer
# exists on this machine alone. A systemd *user* timer runs Claude Code headless
# once a day; it queries GitHub via the project-scoped github MCP server and
# writes a risk-graded summary to the journal (read with
# `journalctl --user -u renovate-review`).
#
# Triage policy:
# * PRs that are clearly low risk (patch/minor bumps to tooling, infra, test
# or framework libs; symmetric diff; CI green; no application logic) AND not
# already approved are AUTO-APPROVED headlessly. These repos enable Renovate
# automerge, so an approval lets the PR merge itself with no human in the
# loop. This is intentional and was explicitly requested.
# * Everything else (medium/high risk, failing/pending CI, stale branches,
# anything touching application logic or needing judgement) is left
# untouched and surfaced to Emma.
#
# The run records two state files under $XDG_STATE_HOME/renovate-review for the
# once-a-day interactive-shell reminder defined below (programs.zsh.initContent):
# `last-run` (date of the last successful run) and `needs-review.txt` (the PRs
# that need Emma's eyes).
#
# Caveats (the foundation this stands on, none of it owned by this flake):
# * Auth is Vertex AI via gcloud Application Default Credentials
# (~/.config/gcloud/application_default_credentials.json). When that token
# can no longer refresh the run fails; re-auth with `gcloud auth login`.
# * The Vertex project, region and model are hardcoded below, copied from the
# interactive environment (the corporate launcher injects them; they live in
# no config file). If IT changes them, update them here. Claude Code handles
# its own network egress, so no proxy is set.
# * The github MCP server is defined in ~/code/.mcp.json, so the job runs with
# that directory as its working directory.
{
config,
pkgs,
lib,
...
}:
let
# The review instructions handed to headless Claude: queue -> drop archived
# repos -> grade risk -> auto-approve the clearly-safe ones, surface the rest.
reviewPrompt = ''
Daily Renovate PR review and triage for Emma-Thorpe_citrix.
1. github MCP search_pull_requests, query: `is:open is:pr review-requested:Emma-Thorpe_citrix author:app/jenkins-stf-jm` (jenkins-stf-jm[bot] is this org's Renovate bot), perPage 50.
2. Build the archived-repo exclusion set: github MCP search_repositories with query `org:csg-citrix-storefront archived:true`, perPage 100, paginate all pages (~128). Collect each archived repo full_name. Do NOT use the `archived:false` qualifier on the PR search itself; it is mis-indexed and returns zero. Filter by the repo set instead.
3. Drop any PR whose repository is in the archived set (e.g. csg-citrix-storefront/traefik-fips is archived; a PR to an archived repo cannot merge and is noise).
4. For each remaining PR: pull_request_read method=get (diff size, mergeable_state, labels, age), method=get_status (CI), and method=get_reviews (existing approvals). Read the body's dependency table for what is bumped.
5. Grade risk Low / Medium / High. LOW means ALL of: only patch or minor version bumps; the packages are tooling, observability, infrastructure, test, or framework/runtime libraries (not business logic); the diff is small and symmetric (version strings / lockfiles only); CI is passing; nothing security-policy-loosening. Anything that is a major bump, touches application logic, has failing or pending CI, is a stale branch needing rebase, or that you are not confident about is NOT Low.
6. AUTO-APPROVE the safe ones: for every PR that is Low risk AND has passing CI AND is not already approved by Emma-Thorpe_citrix, submit an approving review with pull_request_review_write (method=create, event=APPROVE, body: a one-line note that this is an automated approval of a low-risk dependency update). Approve only these. NEVER call merge. NEVER approve a Medium/High PR or one you are unsure about. (Note: these repos automerge on approval, so approval effectively merges it.)
7. Leave for Emma, without approving: every Medium/High risk PR, anything with failing or pending CI, stale branches, and anything needing human judgement.
8. Print a markdown table (PR linked, repo, change summary, size, CI, risk, action: Auto-approved / Needs review / Held) and terse notes. State how many PRs were excluded as archived.
9. As the FINAL lines of your output, emit machine-readable triage lines, one per PR, with these EXACT prefixes and nothing else on the line:
- For each PR you auto-approved: APPROVED> owner/repo#NUMBER short title
- For each PR that needs Emma's review: NEEDS> owner/repo#NUMBER (Risk) one-line reason https://github.com/owner/repo/pull/NUMBER
If no PR needs Emma's review, emit no NEEDS> lines at all.
If the post-filter search returns zero PRs, say so in one line and emit no NEEDS> lines.
'';
# Hold the prompt in its own store file rather than inline, so its literal
# backticks and `$` don't trip shellcheck (SC2016) in the wrapper below.
promptFile = pkgs.writeText "renovate-review-prompt.md" reviewPrompt;
# Tools the headless run is permitted to use without interactive prompts.
# Read-only github MCP calls, plus review_write so it can submit APPROVE
# reviews on low-risk PRs. Deliberately NOT included: any merge tool.
allowedTools = lib.concatStringsSep "," [
"mcp__github-mcp__search_pull_requests"
"mcp__github-mcp__search_repositories"
"mcp__github-mcp__pull_request_read"
"mcp__github-mcp__pull_request_review_write"
];
# Where the run records state for the interactive-shell reminder.
stateDir = "$HOME/.local/state/renovate-review";
renovate-review = pkgs.writeShellApplication {
name = "renovate-review";
runtimeInputs = [ config.programs.claude-code.package ];
text = ''
# The github MCP server is project-scoped to ~/code; run from there.
cd "$HOME/code"
# Claude Code auth + endpoint: Vertex AI. These are injected into the
# interactive shell by the corporate launcher (not present in any config
# file), so a systemd-spawned process must set them explicitly. Do NOT set
# HTTP(S)_PROXY: Claude Code self-provisions its own network egress to
# Vertex; forcing a proxy here points it at a per-session socket that does
# not exist outside an interactive launch and breaks connectivity.
export CLAUDE_CODE_USE_VERTEX=1
export ANTHROPIC_VERTEX_PROJECT_ID=claude-code-citrix
export CLOUD_ML_REGION=global
export ANTHROPIC_MODEL='claude-opus-4-8[1m]'
# Capture the run so we can both log it (journal) and persist the triage
# for the shell reminder. If claude exits non-zero, errexit aborts here and
# the state files are left stale, so the reminder will flag a missed run.
out="$(claude -p "$(cat ${promptFile})" \
--allowedTools ${lib.escapeShellArg allowedTools} \
--output-format text)"
printf '%s\n' "$out"
# Persist state for programs.zsh.initContent's daily reminder. needs-review
# gets the PRs Claude flagged for Emma (the NEEDS> lines, prefix stripped);
# it is empty when nothing needs her attention. grep || true: no matches is
# the all-clear case, not an error.
mkdir -p "${stateDir}"
printf '%s\n' "$out" | grep '^NEEDS> ' | sed 's/^NEEDS> //' > "${stateDir}/needs-review.txt" || true
date +%F > "${stateDir}/last-run"
'';
};
in
{
systemd.user.services.renovate-review = {
Unit.Description = "Daily Renovate PR review (headless Claude Code)";
Service = {
Type = "oneshot";
ExecStart = lib.getExe renovate-review;
};
};
systemd.user.timers.renovate-review = {
Unit.Description = "Schedule the daily Renovate PR review";
Timer = {
OnCalendar = "*-*-* 08:47:00";
# Run on next boot if the machine was off at the scheduled time.
Persistent = true;
# Avoid firing exactly on the minute boundary.
RandomizedDelaySec = "5m";
};
Install.WantedBy = [ "timers.target" ];
};
# Interactive-shell reminder: nudge once per calendar day about the daily
# Renovate timer -- whether it actually ran, and any PRs that need Emma's eyes
# (the auto-approved ones need no nudge). Throttled via a `reminded-on` marker
# so it prints in the first shell/tmux pane of the day, not every pane. mkOrder
# 1600 runs after shell.nix's tmux re-exec (order 200), so it fires inside the
# tmux pane where Emma actually reads it.
programs.zsh.initContent = lib.mkOrder 1600 ''
if [[ $- == *i* ]]; then
__rr_dir="$HOME/.local/state/renovate-review"
__rr_today=$(date +%F)
if [[ "$(cat "$__rr_dir/reminded-on" 2>/dev/null)" != "$__rr_today" ]]; then
__rr_last=$(cat "$__rr_dir/last-run" 2>/dev/null)
if [[ "$__rr_last" != "$__rr_today" ]]; then
print -P "%F{yellow}renovate:%f last review ''${__rr_last:-never} (not today) -- check: systemctl --user status renovate-review"
fi
if [[ -s "$__rr_dir/needs-review.txt" ]]; then
print -P "%F{red}renovate:%f $(grep -c . "$__rr_dir/needs-review.txt") PR(s) need your review:"
sed 's/^/ - /' "$__rr_dir/needs-review.txt"
print -P " -> journalctl --user -u renovate-review -e"
elif [[ "$__rr_last" == "$__rr_today" ]]; then
print -P "%F{green}renovate:%f reviewed today -- low-risk auto-approved, nothing for you."
fi
mkdir -p "$__rr_dir" && print -r -- "$__rr_today" > "$__rr_dir/reminded-on"
fi
unset __rr_dir __rr_today __rr_last
fi
'';
}
+14 -96
View File
@@ -1,6 +1,5 @@
# Interactive shell: zsh + tmux. Wanted on every host.
{
config,
lib,
pkgs,
inputs,
@@ -23,28 +22,12 @@ in
pkgs.ripgrep
pkgs.fd
pkgs.jq
pkgs.btop
pkgs.tea
pkgs.hyperfine # command-line benchmarking
pkgs.sd # saner find-and-replace than sed
];
# Resource monitor, themed Catppuccin Mocha to match the rest of the desktop.
# btop does not bundle the theme, so vendor it from catppuccin/btop (pinned).
programs.btop = {
enable = true;
settings.color_theme = "catppuccin_mocha";
};
xdg.configFile."btop/themes/catppuccin_mocha.theme".source = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/catppuccin/btop/f437574b600f1c6d932627050b15ff5153b58fa3/themes/catppuccin_mocha.theme";
hash = "sha256-THRpq5vaKCwf9gaso3ycC4TNDLZtBB5Ofh/tOXkfRkQ=";
};
programs.zsh = {
enable = true;
# Keep zsh dotfiles under XDG (~/.config/zsh) rather than the legacy $HOME
# layout, matching xdg.enable. history.path is pinned below so the existing
# ~/.zsh_history is reused, not orphaned by the dotDir move.
dotDir = "${config.xdg.configHome}/zsh";
enableCompletion = true;
enableVteIntegration = true;
autosuggestion.enable = true;
@@ -64,9 +47,6 @@ in
];
};
history = {
# Stay at the legacy ~/.zsh_history (default would follow dotDir into
# ~/.config/zsh and orphan the existing file). Keeps history intact.
path = "${config.home.homeDirectory}/.zsh_history";
append = true; # append, don't overwrite, on shell exit
size = 100000; # in-memory (HISTSIZE)
save = 100000; # on-disk (SAVEHIST)
@@ -203,15 +183,12 @@ in
flake = "$HOME/code/nixfiles";
};
# GitHub CLI. `programs.gh.settings` is deliberately unset: home-manager renders
# ~/.config/gh/config.yml as a read-only /nix/store symlink whenever the module
# is enabled, but gh must rewrite that file on `gh auth login` and `gh config
# set`, which then fail with a permission error. Suppress the managed config.yml
# (below) and let gh own it. The token lives in hosts.yml, which is never
# Nix-managed. Set the SSH protocol once at runtime: `gh config set git_protocol
# ssh` (it can't be declarative here without recreating the immutable file).
programs.gh.enable = true;
xdg.configFile."gh/config.yml".enable = lib.mkForce false;
# GitHub CLI. Prefer SSH for any git operations it drives, matching the
# ssh-based remotes used elsewhere.
programs.gh = {
enable = true;
settings.git_protocol = "ssh";
};
programs.tmux = {
enable = true;
@@ -233,7 +210,6 @@ in
sensible
vim-tmux-navigator # Ctrl-h/j/k/l across vim splits and tmux panes
yank
extrakto # prefix+Tab: fzf-grab paths/URLs/text from the pane into the prompt
{
# Catppuccin Mocha statusline (v2 API: flavour + window options must be
# set before the plugin loads, which home-manager does for plugin
@@ -299,7 +275,7 @@ in
# Add the key to the agent on first use, so the passphrase is typed once per
# login session rather than per commit/push (commit signing uses this agent).
# The work box keeps its own ssh config (see work.nix), so this only
# The work box keeps its own ssh config (see work/default.nix), so this only
# manages ~/.ssh/config on the personal hosts.
programs.ssh = {
enable = true;
@@ -348,70 +324,12 @@ in
# enables this in the work module; both being true merges cleanly.
services.ssh-agent.enable = lib.mkIf pkgs.stdenv.hostPlatform.isLinux true;
# Classic process viewer (complements btop). htop has no custom-theme support
# -- only a handful of built-in color schemes -- so it can't be hex-themed like
# btop/bat/fzf. color_scheme = 0 (Default) draws from the terminal's ANSI
# palette, which is Catppuccin Mocha (foot/iTerm2), so it matches by deferring
# to the terminal rather than vendoring a theme.
programs.htop = {
enable = true;
settings = {
color_scheme = 0; # Default -> uses the terminal's Catppuccin palette
delay = 15; # refresh every 1.5s
cpu_count_from_one = 1;
show_cpu_frequency = 1;
show_cpu_usage = 1; # per-core usage shown in the CPU bars
highlight_base_name = 1; # highlight the program name within the path
highlight_megabytes = 1;
highlight_threads = 1;
hide_kernel_threads = 1;
show_program_path = 0; # show just the command, not the full path
tree_view = 1; # start in process-tree mode
tree_view_always_by_pid = 0;
account_guest_in_cpu_meter = 0;
fields = with config.lib.htop.fields; [
PID
USER
PRIORITY
NICE
M_SIZE
M_RESIDENT
M_SHARE
STATE
PERCENT_CPU
PERCENT_MEM
TIME
COMM
];
}
// (
with config.lib.htop;
leftMeters [
(bar "AllCPUs2")
(bar "Memory")
(bar "Swap")
]
)
// (
with config.lib.htop;
rightMeters [
(text "Tasks")
(text "LoadAverage")
(text "Uptime")
]
);
};
# Drop the zsh completion dump on every activation. A stale .zcompdump caches
# /nix/store paths to completion functions; once a rebuild or a manual GC
# removes them, compinit fails with "_git: function definition file not found"
# for every completion. Deleting it forces a fresh rebuild from the current
# fpath on the next shell. compinit dumps to $ZDOTDIR (~/.config/zsh now); the
# $HOME and cache paths are also swept to clear any legacy leftovers.
# Drop the zsh completion dump on every activation. A stale ~/.zcompdump
# caches /nix/store paths to completion functions; once a rebuild or a manual
# GC removes them, compinit fails with "_git: function definition file not
# found" for every completion. Deleting it forces a fresh rebuild from the
# current fpath on the next shell.
home.activation.resetZcompdump = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD rm -f \
"${config.xdg.configHome}"/zsh/.zcompdump* \
"$HOME"/.zcompdump* \
"''${XDG_CACHE_HOME:-$HOME/.cache}"/zsh/.zcompdump* 2>/dev/null || true
$DRY_RUN_CMD rm -f "$HOME"/.zcompdump* "''${XDG_CACHE_HOME:-$HOME/.cache}"/zsh/.zcompdump* 2>/dev/null || true
'';
}
+4 -74
View File
@@ -1,13 +1,11 @@
# Declarative Sway window manager, status bar, lock, idle and notifications.
# Imported via ./desktop.nix, so only graphical hosts get it.
#
# The compositor binary, PAM and the polkit *daemon* come from the system-level
# The compositor binary, PAM and polkit integration come from the system-level
# programs.sway (see ../swaywm.nix); package = null below reuses it instead of
# pulling a second Sway. The polkit authentication *agent* (the thing that draws
# the GUI auth dialog) is a user service started here. home-manager owns the user
# config (~/.config/sway) and wires the systemd user session (sway-session.target),
# which is what lets the agent/swayidle/dunst/kanshi user services start with the
# desktop.
# pulling a second Sway. home-manager owns the user config (~/.config/sway) and
# wires the systemd user session (sway-session.target), which is what lets the
# swayidle/dunst user services start with the desktop.
{
pkgs,
lib,
@@ -101,16 +99,6 @@ in
criteria.app_id = "launcher";
command = "floating enable, resize set 800 500";
}
# Don't let swayidle blank/lock during fullscreen video. Two rules cover
# native Wayland (app_id) and XWayland (class) clients.
{
criteria.app_id = ".*";
command = "inhibit_idle fullscreen";
}
{
criteria.class = ".*";
command = "inhibit_idle fullscreen";
}
];
# Binding modes (submenus). Entered from keybindings below; each action
@@ -289,64 +277,6 @@ in
# an old entry through fuzzel.
services.clipman.enable = true;
# Polkit authentication agent. programs.sway (system) enables the polkit
# daemon but no agent, so GUI privilege prompts (nemo mounting a disk,
# NetworkManager/blueman editing a system resource) would otherwise fail
# silently. lxqt-policykit is a small, toolkit-light agent; bind it to the
# Sway session so it starts and stops with the desktop.
systemd.user.services.polkit-lxqt = {
Unit = {
Description = "lxqt-policykit polkit authentication agent";
PartOf = [ "graphical-session.target" ];
After = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs.lxqt.lxqt-policykit}/bin/lxqt-policykit-agent";
Restart = "on-failure";
};
Install.WantedBy = [ "sway-session.target" ];
};
# Output/display management. Reacts to hotplug and applies per-display
# mode/scale/position. Profiles are hardware-specific: the safe default below
# just enables the internal laptop panel; add docked/desktop profiles with the
# real identifiers from `swaymsg -t get_outputs` (e.g. the Mac Pro's Apple
# Cinema Display with its scale, or a docked laptop + external monitor).
services.kanshi = {
enable = true;
settings = [
{
profile.name = "undocked";
profile.outputs = [
{
criteria = "eDP-1";
status = "enable";
}
];
}
# Example to copy per host (fill in real criteria/mode/scale/position):
# {
# profile.name = "desktop";
# profile.outputs = [
# { criteria = "Apple Computer Inc Cinema HD ..."; mode = "2560x1600"; scale = 1.0; position = "0,0"; status = "enable"; }
# ];
# }
];
};
# Night light. Manual location (no geoclue dependency); adjust the coordinates
# to taste. Warmer at night, neutral by day.
services.gammastep = {
enable = true;
provider = "manual";
latitude = 51.5;
longitude = -0.13; # London-ish; set to your actual location
temperature = {
day = 6500;
night = 3700;
};
};
# fuzzel: the dmenu picker used by clipman, themed Catppuccin Mocha to match
# (fuzzel colours are RRGGBBAA -- 8 hex digits).
programs.fuzzel = {
+1 -22
View File
@@ -3,11 +3,6 @@
{ pkgs, lib, ... }:
{
# Host-scoped extras for this machine only (the EDaaS/WSL host).
imports = [
./renovate-review.nix # daily headless Renovate PR review (systemd user timer)
];
# The work box keeps its own (corporate) ~/.ssh/config; don't let the personal
# programs.ssh (shell.nix) take it over. The ssh-agent below still runs.
programs.ssh.enable = lib.mkForce false;
@@ -42,15 +37,8 @@
pkgs.automake
pkgs.pkg-config
pkgs.wget
pkgs.claude-code
pkgs.google-cloud-sdk
# Day-to-day Kubernetes / Helm / Terraform accelerators for this box.
pkgs.k9s # cluster TUI
pkgs.kubectx # kubectx + kubens (context/namespace switch)
pkgs.stern # multi-pod log tail
pkgs.dyff # semantic YAML/manifest diffs (Helm release drift)
pkgs.tflint # Terraform linter (catches what terraformls won't)
pkgs.terraform-docs # generate Terraform module docs
pkgs.yq-go # jq for YAML
];
services.ssh-agent.enable = true;
home.shellAliases = {
@@ -64,13 +52,4 @@
programs.go = {
enable = true;
};
# LSP servers only relevant to work: C# (omnisharp) and Helm charts (helm_ls).
# The shared editor (lyrathorpe/home/editor.nix) carries the universal ones;
# these are gated to this host so the heavy omnisharp closure stays off the
# personal machines. Tree-sitter grammars (highlighting) remain global there.
programs.nixvim.plugins.lsp.servers = {
omnisharp.enable = true;
helm_ls.enable = true;
};
}
+3 -3
View File
@@ -11,9 +11,9 @@ let
ctp = import ./catppuccin-mocha.nix;
in
{
# The features.swayDesktop.enable option is declared in
# system/modules/features.nix (so headless hosts can read/set it without
# importing this module). This module only provides its implementation.
options = {
features.swayDesktop.enable = lib.mkEnableOption "Enable Sway Desktop";
};
config = lib.mkIf cfg.enable {
programs.sway = {
enable = true;
+13 -41
View File
@@ -80,7 +80,7 @@
};
# Declarative Homebrew for packages with no nixpkgs equivalent or that must be
# the vendor build (GUI casks).
# the vendor build (GUI casks, Mac App Store apps).
homebrew = {
enable = true;
onActivation = {
@@ -97,7 +97,6 @@
"llvm@21"
"lld@21"
"python@3.14"
"dosbox-staging"
];
# GUI applications. macOS app bundles are managed as casks; nixpkgs darwin
# GUI support is unreliable, so these stay on brew for continuity.
@@ -137,45 +136,18 @@
"vscodium"
"winbox"
];
# Mac App Store apps are not managed declaratively: nix-darwin 26.05 forces
# activation to run as root, and `mas` cannot reach the App Store session
# from root, so installs silently fail. Install them by hand with
# `mas install <id>` from a GUI Terminal (the `mas` CLI is in
# environment.systemPackages above).
};
# Touch ID authorises sudo (and darwin-rebuild's sudo prompt) instead of a
# typed password. sudo_local keeps the change in /etc/pam.d/sudo_local so it
# survives macOS updates. reattach pulls in pam_reattach: pam_tid (Touch ID)
# otherwise fails inside tmux/screen because the process is detached from the
# GUI login session -- and terminals here auto-start tmux, so it is required.
security.pam.services.sudo_local = {
touchIdAuth = true;
reattach = true;
};
# Declarative macOS UI defaults -- the main reason to run nix-darwin beyond
# package management. Applied on activation; all reversible.
system.defaults = {
dock = {
show-recents = false;
mru-spaces = false; # don't reorder spaces by use
};
finder = {
AppleShowAllExtensions = true;
ShowPathbar = true;
FXPreferredViewStyle = "Nlsv"; # list view
_FXShowPosixPathInTitle = true;
};
NSGlobalDomain = {
AppleInterfaceStyle = "Dark";
ApplePressAndHoldEnabled = false; # key-repeat instead of the accent popup
InitialKeyRepeat = 15;
KeyRepeat = 2;
};
trackpad = {
Clicking = true; # tap to click
TrackpadThreeFingerDrag = true;
masApps = {
Amphetamine = 937984704;
"Apple Configurator" = 1037126344;
"Game Controller Tester" = 1500593102;
"Home Assistant" = 1099568401;
Infuse = 1136220934;
Keynote = 409183694;
Numbers = 409203825;
Pages = 409201541;
PDFgear = 6469021132;
PL2303Serial = 1624835354;
WireGuard = 1451685025;
};
};
+3 -13
View File
@@ -19,7 +19,9 @@
defaultUser = "emmathorpe";
wslConf.automount.root = "/mnt";
wslConf.interop.appendWindowsPath = true;
wslConf.interop.register = true;
wslConf.interop.enabled = true;
wslConf.interop.includePath = true;
wslConf.network.generateHosts = false;
startMenuLaunchers = true;
docker-desktop.enable = false;
@@ -41,11 +43,6 @@
autoPrune.enable = true;
};
# Match the flake's nixosConfigurations attribute name so `nh os switch`
# (which selects by the local hostname) resolves without an explicit
# -H/--hostname flag. The default would otherwise be the stock NixOS "nixos".
networking.hostName = "emmathorpe-edaas";
networking.resolvconf.enable = false;
# Drop the systemd-ssh-proxy Include from the generated /etc/ssh/ssh_config.
@@ -61,14 +58,7 @@
systemd.services.docker-desktop-proxy.script = lib.mkForce ''${config.wsl.wslConf.automount.root}/wsl/docker-desktop/docker-desktop-user-distro proxy --docker-desktop-root ${config.wsl.wslConf.automount.root}/wsl/docker-desktop "C:\Program Files\Docker\Docker\resources"'';
features.swayDesktop.enable = false;
# Keep this user's systemd --user instance running without an open login
# session, so the home-manager user timer (renovate-review.nix) fires on
# schedule even when no terminal is attached. On WSL the timer still only runs
# while the distro itself is up; Persistent=true catches up a missed run at
# next start.
users.users.emmathorpe.linger = true;
# programs.nix-ld is enabled for all NixOS hosts in common-nixos.nix.
programs.nix-ld.enable = true;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It's perfectly fine and recommended to leave
+1 -1
View File
@@ -12,7 +12,7 @@
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = false;
networking.hostName = "Lyra-Asahi";
networking.hostName = "Emma-Asahi";
# Audio (PipeWire) and the swaylock PAM stack are inherited from
# workstation.nix. hardware.enableRedistributableFirmware is also set there;
@@ -22,10 +22,6 @@
networking.hostName = "MacPro31-NixOS";
# Elderly host: a compressed RAM swap softens memory pressure (earlyoom in
# workstation.nix is the backstop).
zramSwap.enable = true;
# This host accepts SSH, so open 22 (the firewall itself is enabled in
# workstation.nix with a default-deny policy).
services.openssh.enable = true;
-68
View File
@@ -1,68 +0,0 @@
# Raspberry Pi 5 (`lyrathorpe-rpi5`)
Headless `aarch64-linux` server with two roles:
- **Docker host** — daemon exposed over the network (`docker.nix`).
- **nginx reverse proxy** — declarative `virtualHosts` (`reverse-proxy.nix`).
## Install
1. Flash a NixOS `aarch64` SD image (or USB) and boot the Pi. The
`raspberry-pi-5` profile from `nixos-hardware` (wired in the flake host table)
supplies the kernel, firmware and device tree; boot is U-Boot + extlinux.
2. Partition/mount the target, then **regenerate the hardware config on the
device** and replace the committed placeholder:
```sh
nixos-generate-config --root /mnt
# copy /mnt/etc/nixos/hardware-configuration.nix over
# system/machine/RPi5/hardware-configuration.nix in this repo, then commit
```
`hardware-configuration.nix` in this directory is a **placeholder** committed
only so the host evaluates in CI. The machine will not boot correctly until it
is replaced with the generated one.
3. Set the host name to match the flake attribute (already done in
`configuration.nix`: `lyrathorpe-rpi5`) and build:
```sh
sudo nixos-rebuild switch --flake .#lyrathorpe-rpi5
# or, once the hostname is live:
nh os switch
```
4. Give the login user a password (`passwd lyrathorpe`) and confirm the key in
`system/modules/ssh.nix` is the one you will connect with.
## Docker socket (security)
The daemon listens on **plain TCP `2375`, no TLS, no auth**. Access is
root-equivalent on this host. The only protection is the nftables rule in
`docker.nix`, which accepts `2375` **only** from the trusted LAN subnet
(`10.187.1.0/24` by default — change it to match your network). Do not widen
that subnet to anything untrusted.
From a LAN client:
```sh
export DOCKER_HOST=tcp://lyrathorpe-rpi5:2375
docker info
```
The secure upgrade path is mutual TLS on `2376` (`--tlsverify` with a CA and
client certs); it needs out-of-band cert provisioning and is intentionally not
wired here.
## Adding a reverse-proxy site
Each proxied service is a Nix entry in `reverse-proxy.nix`:
```nix
services.nginx.virtualHosts."app.example.lan" = {
# enableACME = true; forceSSL = true; # once a DNS name + cert exist
locations."/" = {
proxyPass = "http://127.0.0.1:8080"; # e.g. a local container
proxyWebsockets = true;
};
};
```
The example vhost is HTTP-only by design. Turn on `enableACME`/`forceSSL`
per-vhost once the host has a real DNS name and the ACME challenge can be met;
`443` is already open in the firewall.
-40
View File
@@ -1,40 +0,0 @@
# Raspberry Pi 5 (aarch64) headless server. Two roles, split into submodules:
# ./docker.nix (Docker host with a network socket) and ./reverse-proxy.nix
# (native nginx). The raspberry-pi-5 nixos-hardware profile (kernel, firmware,
# device tree) and key-only sshd (../../modules/ssh.nix) are layered on in the
# flake host table. Install notes: see ./README.md.
{ ... }:
{
imports = [
./hardware-configuration.nix
./docker.nix
./reverse-proxy.nix
];
# Match the flake's nixosConfigurations attribute name so `nh os switch`
# (which selects by the local hostname) resolves without an explicit -H flag.
networking.hostName = "lyrathorpe-rpi5";
# Headless server: the Sway desktop is intentionally not set up. swaywm.nix is
# not imported and features.swayDesktop.enable defaults to false (declared in
# system/modules/features.nix), so this host keeps plain TTY/SSH login.
# Raspberry Pi boots via U-Boot + extlinux, not GRUB/systemd-boot. The
# raspberry-pi-5 nixos-hardware profile supplies the kernel, firmware and
# device tree.
boot.loader.grub.enable = false;
boot.loader.generic-extlinux-compatible.enable = true;
# Remote administration. Key-only policy and the authorized key come from
# ../../modules/ssh.nix; here we just enable the daemon and open the port.
services.openssh.enable = true;
# Default-deny inbound. Open only SSH here; the Docker and nginx submodules
# open their own ports (Docker via a source-restricted nftables rule, nginx
# via 80/443). List-valued, so these merge with the submodule definitions.
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 ];
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
}
-34
View File
@@ -1,34 +0,0 @@
# Docker host with the daemon socket exposed over the network.
#
# SECURITY: the daemon listens on plain TCP 2375 with NO TLS and NO auth. Access
# to that port is root-equivalent on this host (the Docker API can mount the
# host filesystem and run privileged containers). The ONLY thing protecting it
# is the nftables rule below, which accepts 2375 solely from the trusted LAN
# subnet. Do not widen that subnet to anything you do not fully trust. The
# secure upgrade path is mutual TLS on 2376 (--tlsverify with client certs);
# that needs out-of-band cert provisioning and is intentionally not wired here.
{ ... }:
{
virtualisation.docker.enable = true;
# Expose the daemon over TCP by extending systemd socket activation rather than
# setting daemon.settings.hosts. The NixOS docker unit starts dockerd with
# `-H fd://` and takes its listeners from this socket; putting `hosts` in
# daemon.json as well would conflict with that and dockerd would refuse to
# start. Adding the TCP listener here keeps a single source of truth.
# The leading "" resets the unit's default (unix-socket-only) ListenStream list.
systemd.sockets.docker.socketConfig.ListenStream = [
""
"/run/docker.sock"
"0.0.0.0:2375"
];
# Source-restricted firewall rule for the Docker TCP port. 2375 is deliberately
# NOT added to networking.firewall.allowedTCPPorts (that would open it to every
# source); instead nftables accepts it only from the trusted subnet. Adjust the
# CIDR to match the LAN that should reach the Docker API.
networking.nftables.enable = true;
networking.firewall.extraInputRules = ''
ip saddr 10.187.1.0/24 tcp dport 2375 accept
'';
}
@@ -1,31 +0,0 @@
# PLACEHOLDER hardware configuration for the Raspberry Pi 5.
#
# This file is NOT the real generated config -- it exists only so the host
# evaluates in CI before the Pi is provisioned. The machine will not boot from
# it as-is. On first install, regenerate this file on the device with
# nixos-generate-config --root /mnt
# and replace this placeholder with the output (commit it). See ./README.md.
#
# Like every hardware-configuration.nix in this repo, this file is excluded from
# the formatter and linters (see the pre-commit/treefmt excludes in flake.nix).
{ modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
nixpkgs.hostPlatform = "aarch64-linux";
# The Raspberry Pi 5 boots from an SD card / USB with a FAT firmware partition
# and an ext4 root. Labels match the conventional sd-image layout; the real
# generated config will use by-uuid device paths instead.
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
};
swapDevices = [ ];
}
-39
View File
@@ -1,39 +0,0 @@
# Native nginx reverse proxy. The proxy configuration is declarative Nix:
# every proxied service is an entry under services.nginx.virtualHosts, so the
# whole routing table lives in this file and is built/version-controlled with
# the rest of the system.
#
# To add a proxied service, add another virtualHosts."<host>" entry following
# the example below. To serve it over HTTPS, uncomment enableACME + forceSSL on
# that vhost once it has a real DNS name and the ACME HTTP-01/DNS-01 challenge
# can be satisfied (see security.acme for the account/email and DNS settings).
{ ... }:
{
services.nginx = {
enable = true;
recommendedProxySettings = true; # sane proxy_set_header defaults (Host, X-Forwarded-*)
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
virtualHosts = {
# Example reverse-proxy vhost. Replace the name and upstream with a real
# service (e.g. a container published by the Docker host on this machine).
"example.lan" = {
# enableACME = true; # request a Let's Encrypt cert for this host
# forceSSL = true; # redirect HTTP -> HTTPS once the cert exists
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
proxyWebsockets = true; # forward Upgrade/Connection for WebSocket apps
};
};
};
};
# Public reverse-proxy ports. 443 is opened now so flipping a vhost to TLS
# needs no firewall change.
networking.firewall.allowedTCPPorts = [
80
443
];
}
+1 -11
View File
@@ -1,7 +1,7 @@
# ThinkPad T400 (NixOS). Shared laptop options live in ../../modules/laptop.nix;
# only host-specific settings are here. Install notes (boot variants, GPU,
# partitions): see ./README.md.
{ config, ... }:
{ ... }:
{
imports = [
@@ -31,16 +31,6 @@
# the radeon firmware needed by the discrete GPU below.
hardware.cpu.intel.updateMicrocode = true;
# Battery longevity: cap charging to 75-80%. tlp itself comes from the
# nixos-hardware lenovo-thinkpad profile; tp_smapi supplies the threshold
# sysfs on this 2008-era ThinkPad (kernel-native natacpi is too new for it).
boot.kernelModules = [ "tp_smapi" ];
boot.extraModulePackages = [ config.boot.kernelPackages.tp_smapi ];
services.tlp.settings = {
START_CHARGE_THRESH_BAT0 = 75;
STOP_CHARGE_THRESH_BAT0 = 80;
};
# This T400 has the optional discrete GPU fitted: an ATI Mobility Radeon HD
# 3470 (RV620), driven by the open `radeon` KMS driver. Load it in the initrd
# for early modesetting (clean Sway/Wayland start); firmware comes from
+5 -39
View File
@@ -13,18 +13,6 @@
nix.settings.auto-optimise-store = true;
nix.settings.download-buffer-size = 134217728; # 128 MiB
# Extra binary cache for the nix-community toolchain (home-manager, nixvim,
# treefmt, ...). Merges with any host-specific caches (e.g. the Asahi cache on
# the MBP) rather than replacing them.
nix.settings.substituters = [ "https://nix-community.cachix.org" ];
nix.settings.trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
# Run dynamically-linked foreign binaries (VS Code remote server, prebuilt
# toolchains, language-server downloads) on every NixOS host, not just WSL.
programs.nix-ld.enable = true;
# Minimal system-level CLI available before the home-manager profile loads
# (e.g. early boot / rescue). User-level tooling lives in home-manager.
environment.systemPackages = with pkgs; [
@@ -32,31 +20,9 @@
fastfetch
];
# Fonts on every host. The Nerd Font carries the powerline/Nerd glyphs the
# tmux statusline uses (foot names it explicitly in home/sway.nix); Noto sans +
# colour emoji prevent tofu in terminals/TUIs/Firefox -- important on the WSL
# box, which does not pull the graphical hosts' default Noto stack. The Mac
# installs the Nerd Font via the Darwin config.
fonts.packages = with pkgs; [
nerd-fonts.jetbrains-mono
noto-fonts
noto-fonts-color-emoji
];
# Map the generic fontconfig families so anything asking for "monospace" gets
# the Nerd Font (with emoji fallback), not DejaVu.
fonts.fontconfig.defaultFonts = {
monospace = [
"JetBrainsMono Nerd Font"
"Noto Color Emoji"
];
sansSerif = [
"Noto Sans"
"Noto Color Emoji"
];
serif = [
"Noto Serif"
"Noto Color Emoji"
];
emoji = [ "Noto Color Emoji" ];
};
# Terminal font with powerline/Nerd glyphs. Installed on every host because
# the tmux statusline (which uses these glyphs) runs everywhere, not just on
# the Sway/graphical hosts. foot names it explicitly (home/sway.nix); the Mac
# installs it via the Darwin config.
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
}
-12
View File
@@ -1,12 +0,0 @@
# Feature-flag option declarations shared by every NixOS host (imported via
# baseModules in flake.nix). Declaring the flags here -- rather than inside the
# module that implements them -- means a host can read or set a flag without
# importing the (often large) implementation module. In particular,
# features.swayDesktop.enable is read by lyrathorpe/user.nix on every host, but a
# headless host (e.g. the Pi) must be able to leave it at its default without
# pulling in lyrathorpe/swaywm.nix. The implementation lives in swaywm.nix,
# gated on this flag.
{ lib, ... }:
{
options.features.swayDesktop.enable = lib.mkEnableOption "the Sway desktop";
}
-16
View File
@@ -12,20 +12,4 @@
enable = true;
settings.General.EnableNetworkConfiguration = true;
};
# Lid behaviour: suspend on battery, lock on external power (swayidle's
# before-sleep hook locks before the suspend completes either way).
services.logind.settings.Login = {
HandleLidSwitch = "suspend";
HandleLidSwitchExternalPower = "lock";
};
# Bluetooth. The Asahi MBP loads Apple's BT firmware (see its host config) and
# the T400 has an optional BT module; enable bluez on both, with blueman as the
# GUI/tray manager for the Sway session.
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
};
services.blueman.enable = true;
}
-19
View File
@@ -1,19 +0,0 @@
# Key-only SSH hardening, imported by the hosts that run sshd (T400, Mac Pro).
# The host config still does `services.openssh.enable = true` and opens port 22
# next to where it documents the listening service; this module only tightens
# the policy and installs the authorized key, so a host opting into sshd cannot
# accidentally ship password/root login.
{ username, ... }:
{
services.openssh.settings = {
PasswordAuthentication = false; # keys only
KbdInteractiveAuthentication = false; # no keyboard-interactive fallback
PermitRootLogin = "no";
};
# The key permitted to log in as the primary user. Add more entries here as
# new client machines are provisioned.
users.users.${username}.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDxHvdMTOzpFWUFMtCP7C/4tIOUO3GIO2QPvaifSnWH lyrathorpe@Lyra-MBA"
];
}
+1 -13
View File
@@ -5,16 +5,12 @@
# The bootloader is NOT set here -- it is firmware-specific, not form-factor:
# UEFI hosts (MBP, Mac Pro 3,1) use systemd-boot, the BIOS-only T400 uses GRUB.
# Each machine config declares its own.
{ lib, pkgs, ... }:
{ ... }:
{
features.swayDesktop.enable = true;
console.keyMap = "dvorak";
# Intel thermal management. x86 only -- the Asahi MBP governs its own SoC
# thermals, and thermald is an Intel-platform daemon.
services.thermald.enable = lib.mkIf pkgs.stdenv.hostPlatform.isx86_64 true;
# Default-deny inbound. Hosts that run a listening service open their own
# ports next to where the service is enabled (e.g. sshd -> 22 on X1).
networking.firewall.enable = true;
@@ -24,14 +20,6 @@
services.fstrim.enable = true;
boot.tmp.cleanOnBoot = true;
# Userspace OOM killer: act on memory pressure early instead of letting the
# kernel OOM-thrash. Matters on the 4 GiB T400 and the elderly Mac Pro.
services.earlyoom.enable = true;
# Firmware updates via LVFS. No-op on the Asahi MBP (Apple-managed firmware),
# useful for UEFI/SSD updates on the x86 hosts.
services.fwupd.enable = true;
# Audio. PipeWire with the PulseAudio shim covers every graphical host; no
# per-machine audio config is needed.
services.pipewire = {