Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot b898fd53ed chore(deps): pin dependencies
CI / flake (pull_request) Successful in 2m0s
2026-06-02 15:49:32 +00:00
61 changed files with 528 additions and 4051 deletions
-19
View File
@@ -1,19 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
[*.{nix,yaml,yml,json,md,sh,toml}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
# Markdown uses trailing whitespace for hard line breaks.
[*.md]
trim_trailing_whitespace = false
+9 -74
View File
@@ -1,15 +1,6 @@
# 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]
@@ -20,83 +11,27 @@ jobs:
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install Nix
if: steps.detect.outputs.run == 'true'
uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
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'
# aarch64 hosts evaluate fine on an x86_64 runner; only building would need
# emulation, which we deliberately avoid here.
- name: Evaluate host configurations
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
echo "::endgroup::"
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
echo "::group::eval $host"
nix eval --raw ".#darwinConfigurations.$host.config.system.build.toplevel.drvPath"
echo
echo "::endgroup::"
done
+6 -3
View File
@@ -1,13 +1,16 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":dependencyDashboard", ":semanticCommits"],
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits"
],
"nix": {
"enabled": true
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 6am on monday"],
"automerge": true
"schedule": ["before 6am on monday"]
},
"git-submodules": {
"enabled": false
-85
View File
@@ -1,85 +0,0 @@
# nixfiles
NixOS / nix-darwin / home-manager configuration for all hosts, built from a
single flake.
## Hosts
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) |
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.
## Applying
```sh
# NixOS
sudo nixos-rebuild switch --flake .#<configuration>
# Darwin
darwin-rebuild switch --flake .#lyrathorpe-mac
```
## Shell environment & keybindings
- Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux):
[`lyrathorpe/home/README.md`](./lyrathorpe/home/README.md).
- All Sway / tmux / foot / zsh keyboard shortcuts:
[`lyrathorpe/home/KEYBINDINGS.md`](./lyrathorpe/home/KEYBINDINGS.md).
## Login / greeter
Graphical (Sway) hosts log in through a Wayland greeter — `greetd` running
ReGreet inside the `cage` kiosk compositor — implemented 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
(`passwd <user>`) before it can log in.
## MacBook (Asahi) firmware
The MBP host references `system/modules/firmware/` for Apple peripheral
firmware (Wi-Fi/Bluetooth). These blobs are **committed** (tracked) even though
`.gitignore` lists the directory: the flake is `git+file`, so it only sees
tracked files — untracking them breaks `lyrathorpe-mbp` evaluation (and the CI
host-eval) because the config can't find the firmware. They are not
redistributable; the repo is private.
To refresh them, copy the firmware extracted during the Asahi install (from
`/etc/nixos/firmware`, or re-extract per the
[Asahi NixOS docs](https://github.com/tpwrules/nixos-apple-silicon)) into
`system/modules/firmware/` and commit with `git add -f`.
## Development
A dev shell and a formatting/lint gate are wired through the flake:
- `nix develop` — shell with `deadnix`, `statix`, `treefmt`, and the git
`pre-commit` hooks (installed automatically on first entry).
- `nix fmt` — formats the tree via `treefmt` (nixfmt + shfmt + prettier;
generated files and `flake.lock` are excluded).
- `nix flake check` — runs formatting, `deadnix`, `statix`, the pre-commit
hooks, and evaluates every host. `.editorconfig` carries the base style;
`statix.toml` disables the two house-style lints (`repeated_keys`,
`empty_pattern`).
## 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.
Generated
+38 -303
View File
@@ -1,61 +1,6 @@
{
"nodes": {
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1781226006,
"narHash": "sha256-w4ZTuOnhYiDxjaynrMTASzp802QblBWmo3wpB8wVN4Y=",
"owner": "Homebrew",
"repo": "brew",
"rev": "109191be4988470b51a60a5ef1998520aa24c01b",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "6.0.1",
"repo": "brew",
"type": "github"
}
},
"firefox-addons": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1782014564,
"narHash": "sha256-F/royQHyJAyKWKrV8AaG4Yf1yjzxa+PFk5xvTdvBrzk=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "d6668e34bbce788459883a1097bf0ee170f49c61",
"type": "gitlab"
},
"original": {
"dir": "pkgs/firefox-addons",
"owner": "rycee",
"repo": "nur-expressions",
"type": "gitlab"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1761640442,
"narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=",
@@ -70,7 +15,7 @@
"type": "github"
}
},
"flake-compat_3": {
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1767039857,
@@ -106,70 +51,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",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781733627,
"narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -177,11 +58,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": {
@@ -191,66 +72,28 @@
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781772065,
"narHash": "sha256-xIbRSwDB1GBAUsWsQZUjudGfAGQt3BOpsWaO/ugVa4w=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "adda04f0bf4819575b1978c2f8d78401b3c2be12",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-26.05",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-homebrew": {
"inputs": {
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1781389246,
"narHash": "sha256-ORqLAo/hoJdsZC7UPAuEHev6S0+XIqKEC7vjo5prz1k=",
"owner": "zhaofengli",
"repo": "nix-homebrew",
"rev": "de7953a08ed4bb9245be043e468561c17b89130d",
"type": "github"
},
"original": {
"owner": "zhaofengli",
"repo": "nix-homebrew",
"type": "github"
}
},
"nix-index-database": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1782030356,
"narHash": "sha256-h4WpMr455AfRub0FXBaon6Vcpe0waUyJ4GivIW6oyd4=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "3017088b49efd404f78e3b104f553b97e4af786b",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-index-database",
"type": "github"
}
},
"nixos-apple-silicon": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780294707,
"narHash": "sha256-KZiF/wah9A9N+Pn6t8mScqF0fxLVs+n53/6/IsuqcHM=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "7f4b33118d9d2db87b5ce1ad5152bb727a63e340",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"type": "github"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat_2",
"nixpkgs": [
@@ -258,52 +101,11 @@
]
},
"locked": {
"lastModified": 1781520503,
"narHash": "sha256-XuqQQG1qRyc3o8ld937sDLQNx+QrGV852KJ0dNglJDg=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "43043ad207529650f9fa68e1705f7cf9c08bfdeb",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"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",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781182279,
"narHash": "sha256-V5EQQbDnmdiXGQXrEF1PEL7QYsFqfH8N1E89Z5ONwFk=",
"lastModified": 1780169171,
"narHash": "sha256-3HBYDfBgZ+ph52HS6Ks/bMMwuh2uONIT72sZ1CtLE/s=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "5675822ba756e6e56f8f6a5a76e90e0da2ece94d",
"rev": "998b2821c30b2938637230916904ceb8757c79e8",
"type": "github"
},
"original": {
@@ -314,27 +116,26 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1781216227,
"narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=",
"owner": "nixos",
"lastModified": 1780203844,
"narHash": "sha256-K5sT4jTpGs15ADhviMKNBH38REpPf5Q6mM1+N6cArVE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0",
"rev": "b51242d7d43689db2f3be91bd05d5b24fbb469c4",
"type": "github"
},
"original": {
"owner": "nixos",
"id": "nixpkgs",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
"type": "indirect"
}
},
"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,80 +145,14 @@
"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",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"home-manager": "home-manager",
"nix-darwin": "nix-darwin",
"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": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
"nixpkgs-unstable": "nixpkgs-unstable"
}
}
},
+31 -258
View File
@@ -3,7 +3,7 @@
inputs = {
# Pinned stable channel; the single source of truth for every host.
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
nixpkgs.url = "nixpkgs/nixos-26.05";
# Bleeding-edge channel, used only to pull individual packages via overlay.
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
# Home-manager release matched to the stable nixpkgs; `follows` keeps a single nixpkgs eval.
@@ -15,51 +15,9 @@
# Apple Silicon (Asahi) support for the MacBook host.
nixos-apple-silicon.url = "github:nix-community/nixos-apple-silicon";
nixos-apple-silicon.inputs.nixpkgs.follows = "nixpkgs";
# nix-darwin: manage macOS hosts from this same flake.
nix-darwin.url = "github:nix-darwin/nix-darwin/nix-darwin-26.05";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
# nix-homebrew: declaratively own and install the Homebrew prefix on macOS.
nix-homebrew.url = "github:zhaofengli/nix-homebrew";
# Provides mkFlake: the systems/perSystem scaffolding used below.
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
# Declarative Firefox add-ons (e.g. the Catppuccin theme); see lyrathorpe/user.nix.
firefox-addons = {
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
inputs.nixpkgs.follows = "nixpkgs";
};
# Prebuilt nix-index database so "command not found -> which package
# provides it" works immediately (no manual `nix-index` run). See shell.nix.
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
# treefmt-nix: one multi-language formatter driving `nix fmt` and the
# formatting flake check (nixfmt + shfmt + prettier).
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# git-hooks.nix: declarative pre-commit hooks (nixfmt/deadnix/statix),
# installed into the repo via the devShell.
git-hooks = {
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 =
@@ -70,8 +28,6 @@
home-manager,
nixos-wsl,
nixos-apple-silicon,
nix-darwin,
nix-homebrew,
...
}:
flake-parts.lib.mkFlake { inherit inputs; } (
@@ -79,14 +35,12 @@
let
# claude-code tracks nixpkgs-unstable regardless of the pinned nixpkgs.
overlays = [
(_final: prev: {
inherit
(final: prev: {
claude-code =
(import nixpkgs-unstable {
inherit (prev.stdenv.hostPlatform) system;
config.allowUnfree = true;
})
claude-code
;
}).claude-code;
})
];
@@ -97,32 +51,24 @@
"lens-desktop"
];
# nixpkgs + nix-daemon settings shared by NixOS and Darwin hosts.
commonModule = {
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) unfreePackages;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Make `nix shell nixpkgs#...` and <nixpkgs> use the pinned nixpkgs.
nix.registry.nixpkgs.flake = nixpkgs;
nix.nixPath = [ "nixpkgs=${nixpkgs}" ];
};
# Shared scaffolding for every NixOS host: common user, settings, home-manager.
# Shared scaffolding for every host: common user, overlays, home-manager.
baseModules = [
./lyrathorpe/user.nix
./system/modules/common-nixos.nix
./system/modules/features.nix
commonModule
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) unfreePackages;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Make `nix shell nixpkgs#...` and <nixpkgs> use the pinned nixpkgs.
nix.registry.nixpkgs.flake = nixpkgs;
nix.nixPath = [ "nixpkgs=${nixpkgs}" ];
}
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Back up pre-existing dotfiles (e.g. .zshrc) instead of aborting
# activation when home-manager would overwrite them.
home-manager.backupFileExtension = "backup";
}
];
@@ -138,74 +84,15 @@
fullName,
modules,
homeModules,
# Host form factor. Laptops inherit the default; a desktop host sets
# `portable = false` to drop mobile components (battery block,
# brightness keys) from the home-manager Sway config.
portable ? true,
}:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit
inputs
username
fullName
portable
;
};
specialArgs = { inherit inputs username fullName; };
modules =
baseModules
++ modules
++ [
{
home-manager.extraSpecialArgs = {
inherit
inputs
username
fullName
portable
;
};
home-manager.users.${username}.imports = homeModules;
}
];
};
# Shared scaffolding for every Darwin (macOS) host.
darwinBaseModules = [
commonModule
nix-homebrew.darwinModules.nix-homebrew
home-manager.darwinModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Back up pre-existing dotfiles (e.g. .zshrc) instead of aborting
# activation when home-manager would overwrite them.
home-manager.backupFileExtension = "backup";
}
];
# mkDarwinHost :: { system, username, fullName, modules, homeModules } -> darwinSystem
# Darwin counterpart of mkHost. macOS already owns the login user, so we
# only attach the platform and home-manager; no NixOS user module here.
mkDarwinHost =
{
system,
username,
fullName,
modules,
homeModules,
}:
nix-darwin.lib.darwinSystem {
specialArgs = { inherit inputs username fullName; };
modules =
darwinBaseModules
++ modules
++ [
{
nixpkgs.hostPlatform = system;
# macOS owns the account; point home-manager at its home dir.
users.users.${username}.home = "/Users/${username}";
home-manager.extraSpecialArgs = { inherit inputs username fullName; };
home-manager.users.${username}.imports = homeModules;
}
@@ -222,7 +109,6 @@
fullName = "Lyra Thorpe";
modules = [
./system/machine/MBP-Asahi/configuration.nix
./system/modules/laptop.nix
nixos-apple-silicon.nixosModules.default
./lyrathorpe/swaywm.nix
];
@@ -232,40 +118,12 @@
];
};
lyrathorpe-t400 = {
lyrathorpe-x1c = {
system = "x86_64-linux";
username = "lyrathorpe";
fullName = "Lyra Thorpe";
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 = [
./lyrathorpe/home
./lyrathorpe/home/desktop.nix
];
};
lyrathorpe-macpro31 = {
system = "x86_64-linux";
username = "lyrathorpe";
fullName = "Lyra Thorpe";
portable = false;
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
./system/machine/X1/configuration.nix
./lyrathorpe/swaywm.nix
];
homeModules = [
@@ -285,130 +143,45 @@
];
homeModules = [
./lyrathorpe/home
./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
# ./lyrathorpe/home modules (shell, git, editor) are reused; the Linux-only
# desktop/sway modules are intentionally left out.
darwinHosts = {
lyrathorpe-mac = {
system = "aarch64-darwin";
username = "lyrathorpe";
fullName = "Lyra Thorpe";
modules = [
./system/machine/Darwin/configuration.nix
];
homeModules = [
./lyrathorpe/home
./system/modules/work/default.nix
];
};
};
in
{
# flake-parts modules: treefmt-nix wires `nix fmt` + a formatting check;
# git-hooks.nix wires the pre-commit check + devShell installation script.
imports = [
inputs.treefmt-nix.flakeModule
inputs.git-hooks.flakeModule
];
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
# perSystem is evaluated once per entry in `systems`; `pkgs` is the
# nixpkgs instance for that system. Outputs here become per-system
# attrsets automatically (e.g. devShells.<system>.default).
perSystem =
{ config, pkgs, ... }:
{ pkgs, ... }:
{
# treefmt drives `nix fmt` and the formatting check below. nixfmt
# stays the .nix formatter (the tree is already nixfmt-formatted);
# shfmt covers shell and prettier covers markdown/yaml/json.
treefmt = {
projectRootFile = "flake.nix";
programs.nixfmt.enable = true;
programs.shfmt.enable = true;
programs.prettier.enable = true;
# Generated hardware-configuration.nix files are not hand-edited.
settings.global.excludes = [
"*/hardware-configuration.nix" # generated by nixos-generate-config
"flake.lock" # generated by `nix flake lock`
];
};
# Pre-commit hooks: format + lint gate run on commit. The same hooks
# are exposed as a flake check (pre-commit.check.enable defaults true).
pre-commit.settings = {
# Generated by nixos-generate-config; don't lint/reformat (treefmt
# excludes them too).
excludes = [ "hardware-configuration\\.nix$" ];
hooks = {
nixfmt-rfc-style.enable = true;
deadnix = {
enable = true;
# Unused module args ({config,lib,pkgs,...}) are normal; only
# flag genuinely dead bindings.
settings.noLambdaPatternNames = true;
};
statix.enable = true; # reads statix.toml (repeated_keys/empty_pattern disabled)
};
};
# treefmt-nix exposes its own `checks.treefmt`; alias it to
# `formatting` so the existing CI gate (.#checks.*.formatting) keeps
# working without churn.
checks.formatting = config.treefmt.build.check inputs.self;
# deadnix / statix lints as standalone flake checks so `nix flake
# check` flags dead code and antipatterns independently of pre-commit.
checks.deadnix = pkgs.runCommandLocal "check-deadnix" { nativeBuildInputs = [ pkgs.deadnix ]; } ''
deadnix --fail --no-lambda-pattern-names ${./.} && touch $out
'';
checks.statix = pkgs.runCommandLocal "check-statix" { nativeBuildInputs = [ pkgs.statix ]; } ''
statix check -c ${./.} ${./.} && touch $out
'';
# `nix fmt` formatter for the repo.
formatter = pkgs.nixfmt;
# `nix develop` shell with the tooling needed to hack on this flake.
# shellHook installs the git pre-commit hooks into the working tree.
devShells.default = pkgs.mkShellNoCC {
packages = with pkgs; [
nixfmt
nil
git
deadnix
statix
treefmt
];
shellHook = config.pre-commit.installationScript;
};
checks.formatting =
pkgs.runCommandLocal "check-formatting" { nativeBuildInputs = [ pkgs.nixfmt ]; }
''
# Generated hardware-configuration.nix files are excluded.
nixfmt --check $(find ${./.} -name '*.nix' -not -name 'hardware-configuration.nix') && touch $out
'';
};
# Realise the host tables: each entry becomes a {nixos,darwin}Configuration.
# Realise the host table: each `hosts` entry becomes a nixosConfiguration.
flake.nixosConfigurations = lib.mapAttrs (_name: mkHost) hosts;
flake.darwinConfigurations = lib.mapAttrs (_name: mkDarwinHost) darwinHosts;
}
);
}
-26
View File
@@ -1,26 +0,0 @@
# Catppuccin Mocha palette. Raw 6-digit hex (no leading "#"); consumers add a
# "#" where their format needs it. Shared by the Sway desktop theming
# (home/sway.nix) and the ReGreet greeter (swaywm.nix) so the two stay in sync.
{
base = "1e1e2e";
mantle = "181825";
crust = "11111b";
surface0 = "313244";
surface1 = "45475a";
surface2 = "585b70";
overlay0 = "6c7086";
subtext0 = "a6adc8";
subtext1 = "bac2de";
text = "cdd6f4";
rosewater = "f5e0dc";
red = "f38ba8";
maroon = "eba0ac";
peach = "fab387";
yellow = "f9e2af";
green = "a6e3a1";
teal = "94e2d5";
sapphire = "74c7ec";
blue = "89b4fa";
mauve = "cba6f7";
pink = "f5c2e7";
}
-217
View File
@@ -1,217 +0,0 @@
# Keybindings reference
Every keyboard shortcut configured across this desktop, and where it is defined.
Everything here is managed declaratively through Nix — edit the listed file and
rebuild, never the generated dotfiles.
| Area | Defined in |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| 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**
- **Super** is the `Mod4` / logo (Windows/Command) key; **Alt** is `Mod1`.
- Letter keys are **keysyms** (the character produced), not physical positions.
The keyboard is **Dvorak** (`us`/`dvorak`), so e.g. "Super+s" is whatever key
types `s` in Dvorak.
- Shortcuts apply to every Sway host (MBP, T400, Mac Pro); brightness keys are
laptop-only, as noted.
---
## Sway
### Applications & session
| Shortcut | Action |
| ------------------- | ------------------------------------------------------- |
| `Super`+`Return` | Open a terminal (foot) |
| `Super`+`Space` | App launcher (sway-launcher-desktop in a floating foot) |
| `Super`+`d` | App launcher (same as above; module default) |
| `Super`+`e` | File manager (nemo) |
| `Super`+`c` | Clipboard history picker (clipman → fuzzel) |
| `Super`+`l` | Lock screen (swaylock) |
| `Super`+`Shift`+`q` | Close the focused window |
| `Super`+`Shift`+`c` | Reload the Sway config |
| `Super`+`Shift`+`e` | Exit Sway (asks for confirmation) |
### Focus
| Shortcut | Action |
| ----------------------- | ---------------------------------------- |
| `Super`+`←`/`↓`/`↑`/`→` | Move focus by direction |
| `Super`+`h`/`j`/`k` | Move focus left / down / up (vim-style) |
| `Super`+`a` | Focus the parent container |
| `Super`+`Alt`+`Space` | Toggle focus between tiling and floating |
> Note: vim focus-right would be `Super`+`l`, but that is bound to **lock** here;
> use `Super`+`→`.
### Moving windows
| Shortcut | Action |
| ------------------------------- | ---------------------------------------- |
| `Super`+`Shift`+`←`/`↓`/`↑`/`→` | Move the window by direction |
| `Super`+`Shift`+`h`/`j`/`k`/`l` | Move the window left / down / up / right |
| `Super`+`Shift`+`Space` | Toggle the window floating |
Mouse (with `Super` held): left-drag moves a window, right-drag resizes it.
### Layout
| Shortcut | Action |
| ----------- | -------------------------------------------------------------------------------------- |
| `Super`+`b` | Split horizontally |
| `Super`+`v` | Split vertically |
| `Super`+`s` | Stacking layout |
| `Super`+`w` | Tabbed layout |
| `Super`+`f` | Toggle fullscreen |
| `Super`+`y` | **Layout submenu**: `s` stacking · `w` tabbed · `e` toggle split · `Return`/`Esc` exit |
> The layout submenu's `e` (toggle split) is the home for that action since
> `Super`+`e` now opens the file manager.
### Workspaces
| Shortcut | Action |
| ----------------------- | --------------------------------- |
| `Super`+`1``0` | Switch to workspace 1…10 |
| `Super`+`Shift`+`1``0` | Move the window to workspace 1…10 |
| `Super`+`z` | Previous workspace |
| `Super`+`x` | Next workspace |
### Scratchpad
| Shortcut | Action |
| ------------------- | --------------------------------- |
| `Super`+`Shift`+`-` | Move the window to the scratchpad |
| `Super`+`-` | Show / cycle the scratchpad |
### Modes (submenus)
| Shortcut | Action |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `Super`+`r` | **Resize mode**: arrow keys resize; `Return`/`Esc` exit |
| `Super`+`y` | **Layout mode** (see Layout above) |
| `Super`+`Shift`+`x` | **Power menu**: `l` lock · `e` log out · `s` sleep · `r` reboot · `Shift`+`s` shutdown · `Return`/`Esc` exit |
### Screenshots
| Shortcut | Action |
| --------------- | ---------------------------------------- |
| `Print` | Select a region → swappy (annotate/save) |
| `Shift`+`Print` | Focused window → swappy |
### Audio & media
| Shortcut | Action |
| ----------------------------------------------- | ---------------------- |
| `XF86AudioRaiseVolume` / `XF86AudioLowerVolume` | Volume ±5% (wpctl) |
| `XF86AudioMute` | Toggle output mute |
| `XF86AudioMicMute` | Toggle microphone mute |
| `XF86AudioPlay` | Play/pause (playerctl) |
| `XF86AudioNext` / `XF86AudioPrev` | Next / previous track |
### Brightness — laptops only
| Shortcut | Action |
| ----------------------------------------------- | ----------------------------- |
| `XF86MonBrightnessUp` / `XF86MonBrightnessDown` | Backlight ±5% (brightnessctl) |
Present only on portable hosts (T400, MBP); desktops have no internal backlight.
---
## tmux
Prefix is **`Ctrl`+`b`** (default). Copy mode uses **vi** keys.
| Shortcut | Action |
| --------------------------------------- | -------------------------------------------------------------------------------------------- |
| `Ctrl`+`b` then `v` | Split into left/right panes |
| `Ctrl`+`b` then `s` | Split into top/bottom panes |
| `Ctrl`+`h`/`j`/`k`/`l` | Move between panes — and into/out of vim splits — seamlessly (vim-tmux-navigator, no prefix) |
| `Alt`+`←`/`→`/`↑`/`↓` | Switch pane by direction (no prefix needed) |
| `Ctrl`+`b` then `[` | Enter copy mode (then vi motions; `Space`/`Enter` to select/copy) |
| `Ctrl`+`b` then `z` | Zoom / unzoom the focused pane |
| `Ctrl`+`b` then `c` | New window |
| `Ctrl`+`b` then `n` / `p` | Next / previous window |
| `Ctrl`+`b` then `d` | Detach |
| `Ctrl`+`b` then `Ctrl`+`s` / `Ctrl`+`r` | Save / restore the session (resurrect; continuum also auto-saves and restores on start) |
| Mouse | Enabled — click to focus, drag borders, scroll, select |
> The stock split keys `%` and `"` are unbound; use `v` / `s` above. `Ctrl`+`b`
> then `s` is therefore a split, not the session tree.
>
> Sessions persist across reboots (resurrect + continuum). Terminals auto-start
> tmux; `NO_TMUX=1 <terminal>` opens a bare shell instead.
---
## foot (terminal)
Only colours are themed; these are foot's default key bindings.
| Shortcut | Action |
| --------------------------------------- | ----------------------------- |
| `Ctrl`+`Shift`+`c` / `Ctrl`+`Shift`+`v` | Copy / paste (clipboard) |
| `Shift`+`Insert` | Paste primary selection |
| `Ctrl`+`Shift`+`r` | Search scrollback |
| `Ctrl`+`+` / `Ctrl`+`-` / `Ctrl`+`0` | Font larger / smaller / reset |
| `Ctrl`+`Shift`+`u` | URL mode (jump to/open links) |
| `Ctrl`+`Shift`+`n` | Spawn a new terminal |
| `Shift`+`PageUp` / `Shift`+`PageDown` | Scroll back / forward |
---
## 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 |
| --------- | -------------------------------------------------------------------------------------------------- |
| `↑` / `↓` | History **substring** search — type a fragment first, then the arrows cycle matching past commands |
Bound for both CSI and SS3 cursor sequences, so it works in foot, iTerm2 and
the Linux TTY alike.
-213
View File
@@ -1,213 +0,0 @@
# Interactive shell environment
Everything the shell, terminal multiplexer, git and ssh do beyond their defaults,
and where each is defined. All of it is managed declaratively through
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) |
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).
---
## 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 |
**Aliases:** `ls`/`ll`/`la`/`lt``eza` (icons + git), `cls``clear`. git aliases live in git.nix (below).
## CLI tools
| Tool | What it gives you |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fzf` | `Ctrl-R` fuzzy history, `Ctrl-T` file picker, `Alt-C` fuzzy cd (Catppuccin-themed) |
| `zoxide` | `z <fragment>` jumps to frecent directories |
| `direnv` + `nix-direnv` | per-project environments auto-loaded on `cd` (cached Nix dev shells) |
| `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 |
| `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.
**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`).
## tmux
**Auto-start:** opening any interactive terminal — foot, iTerm2, the WSL shell, the
Linux console — drops you straight into a tmux session named `main` (attach if it
exists, else create). Panes run a plain non-login zsh. It deliberately does **not**
fire for SSH sessions, VS Code's integrated terminal, already-inside-tmux, or
non-interactive shells. Escape hatch: `NO_TMUX=1 <terminal>` opens a bare shell.
| Setting | Value |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Mode keys | vi |
| Mouse | on |
| Scrollback | 500000 lines |
| `escape-time` | 10ms (the 500ms default lagged vim's ESC) |
| `focus-events` | on (vim autoread) |
| `base-index` / `pane-base-index` | 1 |
| Splits | `prefix s` vertical, `prefix v` horizontal (stock `%`/`"` unbound) |
| Pane nav | `Alt`+arrows (no prefix) |
| Terminal | `default-terminal tmux-256color`; truecolor advertised per outer terminal (`foot*`, `xterm-256color`/iTerm2) via `terminal-features … RGB` |
| 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`
(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.
## 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.
| 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 |
| Behaviour | |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Pulls | rebase, with autostash + autosquash |
| Fetch | prune deleted remote branches |
| Conflicts | `zdiff3` (shows the common ancestor) |
| Diffs | histogram algorithm, colour-moved |
| `rerere` | remembers + replays conflict resolutions |
| Commit editor | full diff shown (`commit.verbose`) |
| Misc | branches sorted by date, `column.ui = auto`, `help.autocorrect = prompt`, `push.autoSetupRemote` |
| Global ignores | `result`, `result-*`, `.direnv`, `*.swp`, `.DS_Store` |
| Signing | SSH commit + tag signing (`mkDefault`, so a host without the key in its agent can disable it). Personal email `iam@emmathe.dev`; the work box overrides email + signing. |
## ssh
| Feature | Notes |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| ssh-agent | runs on Linux (launchd on macOS); keys added on **first use** so the passphrase is typed once per login session — this also feeds git commit signing |
| macOS | `UseKeychain` caches the passphrase in the login keychain (guarded by `IgnoreUnknown`, so a non-Apple `ssh` skips it instead of erroring) |
| Gitea remote | `code.emmathe.dev``HostName 10.187.1.76` (DNS-override), `Port 30009`, user `git`, dedicated key, `identitiesOnly` |
| Defaults | the module's deprecated default block is opted out; equivalents kept under `settings."*"` |
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
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` /
`nix-collect-garbage -d`) when no important session is running.
## Per-host differences
| | Personal Linux (sway) | macOS | Work WSL (EDaaS) |
| --------------------------- | --------------------- | ----------------- | --------------------------- |
| Auto-tmux | yes (foot/TTY) | yes (iTerm2) | yes (WSL shell) |
| git email | `iam@emmathe.dev` | `iam@emmathe.dev` | `…@citrix.com` (work) |
| ssh config managed | yes | yes | no (keeps corporate config) |
| ssh-agent | yes | launchd | yes (work module) |
| GUI / theming (desktop.nix) | yes | no | no |
-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.
+1 -22
View File
@@ -1,34 +1,13 @@
# 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
# (bat themes, gh config, ...) write under xdg.configHome; enabling this
# makes the paths explicit and consistent across hosts. No regression: the
# 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.
home.sessionVariables = {
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.
MANPAGER = "sh -c 'col -bx | bat -l man -p'";
};
# Pinned to the release first installed on these hosts, NOT the current
# nixpkgs (26.05). stateVersion freezes stateful defaults (file locations,
# service data formats) to that release; bumping it silently migrates that
# state and can break it. Leave it -- it is intentional, not stale.
home.stateVersion = "25.05";
}
+10 -93
View File
@@ -1,14 +1,7 @@
# Graphical desktop layer: GUI apps, Wayland session env, and cursor theme.
# Imported only on hosts that run Sway (MBP, T400, Mac Pro); never pulled onto
# the headless WSL host. Login (and the Sway session launch) is handled by the
# greetd/ReGreet greeter -- see ../swaywm.nix -- so there is no tty1 autostart.
{
pkgs,
config,
inputs,
username,
...
}:
# Graphical desktop layer: GUI apps, Wayland session env, cursor theme, and the
# tty1 Sway autostart. Imported only on hosts that run Sway (MBP, X1); never
# pulled onto the headless WSL host.
{ pkgs, lib, ... }:
{
imports = [
./sway.nix
@@ -17,7 +10,6 @@
home.packages = [
pkgs.element-desktop
pkgs.legcord
pkgs.nemo # file manager (launched via Mod+e, see ./sway.nix)
#pkgs.plex-desktop
#pkgs.plexamp
];
@@ -27,56 +19,6 @@
XDG_CURRENT_DESKTOP = "sway";
};
# Default apps for the desktop (writes ~/.config/mimeapps.list). Firefox owns
# the web; nemo owns directories/file URIs; images, PDFs and plain text open
# in Firefox too -- no dedicated GUI viewer/editor is installed and vim is
# terminal-only (no usable GUI .desktop for double-click handoff). Kept
# minimal -- only the handlers actually present on these hosts.
xdg.mimeApps = {
enable = true;
defaultApplications = {
"text/html" = "firefox.desktop";
"x-scheme-handler/http" = "firefox.desktop";
"x-scheme-handler/https" = "firefox.desktop";
"x-scheme-handler/about" = "firefox.desktop";
"x-scheme-handler/unknown" = "firefox.desktop";
"inode/directory" = "nemo.desktop";
"image/png" = "firefox.desktop";
"image/jpeg" = "firefox.desktop";
"image/gif" = "firefox.desktop";
"image/webp" = "firefox.desktop";
"image/svg+xml" = "firefox.desktop";
"application/pdf" = "firefox.desktop";
"text/plain" = "firefox.desktop";
};
};
# Theme GTK apps (nemo, etc.) to match the Catppuccin Mocha desktop. Under
# Sway there is no XSettings daemon, so GTK reads these from the generated
# ~/.config/gtk-{3,4}.0/settings.ini directly. The Mocha theme is dark by
# design, so no separate prefer-dark hint is needed.
gtk = {
enable = true;
# Theme GTK4 apps too (for any added later). GTK4 ignores gtk-theme-name,
# but home-manager turns this into an `@import` of the theme's
# gtk-4.0/gtk.css into ~/.config/gtk-4.0/gtk.css -- which even libadwaita
# honours, since that file overrides the named colours it uses
# (window_bg_color, accent_bg_color, ...). Set explicitly (same value as the
# legacy default) so it also silences the stateVersion<26.05 warning.
gtk4.theme = config.gtk.theme;
theme = {
name = "catppuccin-mocha-blue-standard";
package = pkgs.catppuccin-gtk.override {
accents = [ "blue" ];
variant = "mocha";
};
};
iconTheme = {
name = "Adwaita";
package = pkgs.adwaita-icon-theme;
};
};
home.pointerCursor = {
gtk.enable = true;
x11 = {
@@ -88,35 +30,10 @@
size = 24;
};
# Firefox is themed at the browser level (it does not follow the GTK theme).
# The system installs the binary (programs.firefox in ../user.nix); here
# home-manager owns only the profile, hence package = null. Apply the
# Catppuccin Mocha theme add-on (only the mauve accent is packaged upstream;
# the rest of the desktop uses blue) and make content + UI dark.
programs.firefox = {
enable = true;
package = null;
# Keep the legacy profile location (~/.mozilla/firefox) -- that is where the
# system Firefox actually looks; pin it explicitly to silence the
# stateVersion<26.05 default-change warning (the new XDG path depends on
# Firefox's own profile support).
configPath = ".mozilla/firefox";
profiles.${username} = {
id = 0;
isDefault = true;
extensions = {
force = true;
packages = [
inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}.catppuccin-mocha-mauve
];
};
settings = {
# Enable bundled add-ons automatically so the theme applies on first run.
"extensions.autoDisableScopes" = 0;
# Dark chrome + dark page content.
"ui.systemUsesDarkTheme" = 1;
"layout.css.prefers-color-scheme.content-override" = 0;
};
};
};
# Start Sway automatically on the first virtual terminal.
programs.zsh.initContent = lib.mkOrder 1500 ''
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
exec sway
fi
'';
}
+20 -187
View File
@@ -1,197 +1,30 @@
# 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
];
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
if v:version < 802
packadd! peaksea
endif
syntax enable
colorscheme peaksea
set termguicolors
set background=dark
au BufNewFile,BufRead *Jenkinsfile setf groovy
'';
};
}
+7 -99
View File
@@ -1,14 +1,6 @@
# Version control: git + delta pager + commitizen + lazygit. The work host
# layers commit signing and an email override on top (see work.nix).
{
pkgs,
lib,
fullName,
...
}:
let
ctp = import ../catppuccin-mocha.nix;
in
# Version control: git + delta pager + commitizen. The work host layers
# commit signing and an email override on top (see work/default.nix).
{ pkgs, fullName, ... }:
{
home.packages = [
pkgs.commitizen
@@ -19,101 +11,17 @@ in
package = pkgs.gitFull;
settings = {
user.name = fullName;
# Personal identity. mkDefault so the work module overrides it on the work
# host (and to merge cleanly with that plain definition there).
user.email = lib.mkDefault "iam@emmathe.dev";
push.autoSetupRemote = true;
init.defaultBranch = "main";
# Rebase-centric pulls (matches the "always a branch, linear history"
# workflow); stash/restore and reorder fixups automatically.
pull.rebase = true;
rebase = {
autoStash = true;
autoSquash = true;
push = {
autoSetupRemote = true;
};
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";
colorMoved = "default";
init = {
defaultBranch = "main";
};
rerere.enabled = true; # remember + replay conflict resolutions
# delta pager config (programs.delta is enabled below, with git
# integration; these keys land under [delta] in the git config).
# syntax-theme reuses the Catppuccin Mocha tmTheme vendored for bat in
# shell.nix -- delta reads bat's theme directory.
delta = {
syntax-theme = "Catppuccin Mocha";
navigate = true; # n/N to jump between diff hunks
line-numbers = true;
side-by-side = true;
};
commit.verbose = true; # full diff in the commit-message editor
branch.sort = "-committerdate"; # most-recent branches first
column.ui = "auto";
help.autocorrect = "prompt";
alias = {
st = "status";
co = "checkout";
sw = "switch";
br = "branch";
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.
cz = "!cz";
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.
gpg.format = "ssh";
user.signingkey = lib.mkDefault "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDxHvdMTOzpFWUFMtCP7C/4tIOUO3GIO2QPvaifSnWH lyrathorpe@Lyra-MBA";
commit.gpgsign = lib.mkDefault true;
tag.gpgsign = lib.mkDefault true;
};
# Global ignore file (~/.config/git/ignore).
ignores = [
"result"
"result-*"
".direnv"
"*.swp"
".DS_Store"
];
};
programs.delta = {
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
'';
}
+17 -384
View File
@@ -1,417 +1,50 @@
# Interactive shell: zsh + tmux. Wanted on every host.
{ lib, ... }:
{
config,
lib,
pkgs,
inputs,
...
}:
let
# Shared Catppuccin Mocha palette: raw 6-hex strings, no leading "#".
ctp = import ../catppuccin-mocha.nix;
in
{
imports = [
# Prebuilt nix-index database -> working command-not-found
# ("cmd not found -> which nix package provides it"), no manual indexing.
inputs.nix-index-database.homeModules.default
];
# CLI staples wanted on every host (search, parse, monitor). ripgrep/fd also
# back fzf and editor integrations; tea is the Gitea CLI for code.emmathe.dev.
home.packages = [
pkgs.ripgrep
pkgs.fd
pkgs.jq
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;
# Bind Up/Down for history-substring-search in BOTH cursor-key modes: CSI
# (^[[A/^[[B -- normal mode, and what the Linux TTY sends) and SS3
# (^[OA/^[OB -- application mode, used by foot, tmux and iTerm2). Binding
# only the default CSI form leaves it dead at the prompt in foot/iTerm2.
historySubstringSearch = {
enable = true;
searchUpKey = [
"^[[A"
"^[OA"
];
searchDownKey = [
"^[[B"
"^[OB"
];
};
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)
ignoreDups = true; # drop consecutive duplicates
ignoreSpace = true; # leading-space commands stay out of history
expireDuplicatesFirst = true;
share = true; # live-share history across sessions
extended = true; # record timestamps
};
historySubstringSearch.enable = true;
history.append = true;
oh-my-zsh = {
enable = true;
plugins = [
"git"
"man"
"sudo" # double-Esc prefixes the last command with sudo
"colored-man-pages"
"extract" # `extract <archive>` for any format
"history-substring-search"
];
theme = "robbyrussell";
};
syntaxHighlighting.enable = true;
initContent = lib.mkMerge [
# Auto-start tmux in every interactive terminal -- foot, iTerm2, the WSL
# shell, the Linux console -- so a new terminal lands straight in the
# multiplexer (session "main": attach if present, else create). Panes run
# a plain non-login zsh (tmux's default-command "${SHELL}"). Order 200
# runs before oh-my-zsh/compinit so the exec replaces the shell before
# that setup is wasted. Guards, each preventing a real breakage:
# interactive only -> don't hijack scp / `ssh host cmd` / scripted shells
# $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop)
# not SSH -> don't force inbound SSH logins into a server tmux
# not VS Code -> its integrated terminal manages itself
# tmux on PATH -> a failed exec would otherwise kill the login shell
# $NO_TMUX unset -> escape hatch: `NO_TMUX=1 <term>` opens a bare shell
(lib.mkOrder 200 ''
if [[ $- == *i* ]] \
&& [[ -z "$TMUX" ]] \
&& [[ -z "$NO_TMUX" ]] \
&& [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \
&& [[ "$TERM_PROGRAM" != "vscode" ]] \
&& command -v tmux >/dev/null 2>&1; then
exec tmux new-session -A -s main
fi
'')
# Prefix the prompt with the hostname over SSH (mkAfter).
(lib.mkOrder 1500 ''
if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then
export PS1="%M $PS1"
fi
'')
];
shellAliases = {
# eza's zsh integration also defines these; set explicitly so the
# icons/git intent is obvious.
ls = "eza --icons --git";
ll = "eza --icons --git -l";
la = "eza --icons --git -la";
lt = "eza --icons --git --tree";
cls = "clear";
};
# Prefix the prompt with the hostname over SSH. The graphical autostart
# (exec sway on tty1) lives in ./desktop.nix so it never runs on headless
# hosts.
initContent = lib.mkOrder 1500 ''
if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then
export PS1="%M $PS1"
fi
'';
envExtra = ''
alias cls=clear
'';
};
# Fuzzy finder: Ctrl-R fuzzy history, Ctrl-T files, Alt-C cd.
programs.fzf = {
enable = true;
enableZshIntegration = true;
# Catppuccin Mocha colours (rendered into FZF_DEFAULT_OPTS --color). Each
# value needs a leading "#"; the palette stores raw hex.
colors = {
"bg" = "#${ctp.base}";
"bg+" = "#${ctp.surface1}"; # current line / selected row
"fg" = "#${ctp.text}";
"fg+" = "#${ctp.text}";
"hl" = "#${ctp.blue}"; # match highlights
"hl+" = "#${ctp.blue}";
"header" = "#${ctp.red}";
"info" = "#${ctp.mauve}";
"marker" = "#${ctp.green}";
"pointer" = "#${ctp.pink}";
"prompt" = "#${ctp.mauve}";
"spinner" = "#${ctp.pink}";
"border" = "#${ctp.surface1}";
};
};
# Frecency directory jumping: `z <fragment>`.
programs.zoxide = {
enable = true;
enableZshIntegration = true;
};
# Per-project environments auto-loaded on cd, with the Nix dev-shell cache.
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
# Modern ls (drives the ls aliases above).
programs.eza = {
enable = true;
git = true;
icons = "auto"; # boolean form is deprecated
};
# Syntax-highlighting pager, used as `bat` (acts like cat when piped). bat
# ships no Catppuccin theme, so vendor the upstream tmTheme from catppuccin/bat
# (delta in git.nix reuses it as its syntax-theme).
programs.bat = {
enable = true;
config.theme = "Catppuccin Mocha";
themes."Catppuccin Mocha" = {
src = pkgs.fetchFromGitHub {
owner = "catppuccin";
repo = "bat";
rev = "6810349b28055dce54076712fc05fc68da4b8ec0";
sha256 = "1y5sfi7jfr97z1g6vm2mzbsw59j1jizwlmbadvmx842m0i5ak5ll";
};
file = "themes/Catppuccin Mocha.tmTheme";
};
};
# command-not-found backed by the prebuilt nix-index DB (module imported
# above). `comma` runs an uninstalled program once: `, cowsay hi`.
programs.nix-index.enable = true;
programs.nix-index-database.comma.enable = true;
# Nicer nixos-rebuild/home-manager (diffs) + $NH_FLAKE. No automatic clean:
# the scheduled GC's only benefit is reclaiming disk, but it can reap store
# paths the current generation still references (notably on nix-darwin, where
# it broke completion by removing an in-use oh-my-zsh). GC manually instead:
# `nh clean all` / `nix-collect-garbage -d` when nothing important is running.
programs.nh = {
enable = true;
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;
programs.tmux = {
enable = true;
reverseSplit = true;
# tmux-256color (not tmux-direct): the standard inside-tmux terminfo.
# tmux-direct's capabilities desync zsh's line redraw on some terminals
# (e.g. iTerm2 -> duplicated chars on Tab, stray newlines). Truecolor is
# advertised per outer terminal via the RGB terminal-features below.
terminal = "tmux-256color";
terminal = "tmux-direct";
newSession = true;
keyMode = "vi";
historyLimit = 500000;
historyLimit = 50000;
mouse = true;
escapeTime = 10; # was the 500ms default -> laggy ESC in vim
focusEvents = true; # let vim see focus changes (autoread)
baseIndex = 1; # sets both base-index and pane-base-index
plugins = with pkgs.tmuxPlugins; [
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
# extraConfig; the status modules below go in the main extraConfig,
# which HM appends after all plugins).
plugin = catppuccin;
extraConfig = ''
set -g @catppuccin_flavor 'mocha'
set -g @catppuccin_window_status_style 'rounded'
'';
}
resurrect # save/restore sessions
{
plugin = continuum; # auto-save + restore on tmux start (after resurrect)
extraConfig = ''
set -g @continuum-restore 'on'
'';
}
];
# `reverseSplit = true` already binds s -> vertical and v -> horizontal
# split (the dotfiles' vim-style splits).
extraConfig = ''
# Run a non-login shell in new panes/windows.
set -g default-command "''${SHELL}"
# Drop the stock split keys in favour of the s/v binds above.
unbind %
unbind '"'
# Alt+Arrow pane navigation
bind -n M-Left select-pane -L
bind -n M-Right select-pane -R
bind -n M-Up select-pane -U
bind -n M-Down select-pane -D
# Truecolor for the outer terminals (foot reports xterm-ish too; iTerm2 is
# xterm-256color). Without this, with tmux-256color as default-terminal,
# 24-bit colour would be quantised to 256.
set -as terminal-features ",xterm-256color:RGB"
# Tell tmux which capabilities the foot terminal supports, so truecolor,
# synchronised output, the system clipboard (OSC 52), window titles and
# cursor styling all pass through.
set -as terminal-features ",foot*:RGB"
set -as terminal-features ",foot*:sync"
set -as terminal-features ",foot*:clipboard"
set -as terminal-features ",foot*:title"
set -as terminal-features ",foot*:ccolour"
set -as terminal-features ",foot*:cstyle"
# No home-manager options for these.
set -g renumber-windows on
set -g set-clipboard on
# Catppuccin v2 statusline. Must run after the plugin has loaded;
# home-manager appends this extraConfig after the whole plugin list.
set -g status-left-length 100
set -g status-right-length 100
set -g status-left ""
set -g status-right "#{E:@catppuccin_status_application}"
set -ag status-right "#{E:@catppuccin_status_session}"
'';
};
# 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
# manages ~/.ssh/config on the personal hosts.
programs.ssh = {
enable = true;
# The module's built-in default "*" block is being deprecated; opt out and
# carry the defaults we want ourselves under settings."*".
enableDefaultConfig = false;
settings = {
# Global defaults (rendered last, as ssh_config wants). AddKeysToAgent
# adds the key on first use so the passphrase is typed once per session.
"*" = {
AddKeysToAgent = "yes";
ForwardAgent = false;
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
}
# macOS: also cache the passphrase in the login keychain. UseKeychain
# exists only in Apple's ssh; nixpkgs' openssh (which may be the `ssh` on
# PATH) rejects it as "Bad configuration option". IgnoreUnknown (emitted
# first by the module) makes any non-Apple ssh skip it instead of erroring,
# while Apple's ssh still honours it. Darwin-only.
// lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin {
IgnoreUnknown = "UseKeychain";
UseKeychain = "yes";
};
# Gitea remote (the flake's origin) -- required on every host. HostName
# pins the IP so it resolves without DNS. Port 30009 is non-default; pin
# the dedicated key (identitiesOnly avoids "too many authentication
# failures" when the agent holds several keys).
"code.emmathe.dev" = {
HostName = "10.187.1.76";
User = "git";
Port = 30009;
IdentityFile = "~/.ssh/code.emmathe.dev";
IdentitiesOnly = true;
};
};
};
# Run a user ssh-agent on Linux (macOS provides one via launchd). EDaaS also
# 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.
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
'';
}
+20 -413
View File
@@ -1,40 +1,12 @@
# 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.
{
pkgs,
lib,
# Threaded from mkHost (flake.nix). Desktop hosts set this false to drop
# mobile components (battery block, screen-brightness keys).
portable ? true,
...
}:
let
# Catppuccin Mocha (shared with the ReGreet greeter). Raw hex; prefix "#"
# where a consumer needs it -- Sway/i3status/dunst want "#", foot/swaylock do
# not.
ctp = import ../catppuccin-mocha.nix;
# Focused-window screenshot -> swappy editor (the dotfiles' grimshot.sh logic).
# Full store paths so it needs nothing on PATH.
screenshotWindow = pkgs.writeShellScript "screenshot-window" ''
${pkgs.grim}/bin/grim -g "$(${pkgs.sway}/bin/swaymsg -t get_tree \
| ${pkgs.jq}/bin/jq -r '.. | select(.focused?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"')" \
- | ${pkgs.swappy}/bin/swappy -f -
'';
# Binding-mode names. The string is both the `modes` attr key and what the
# bar's mode indicator shows, so the keys are spelled out in the label.
layoutMode = "layout: [s]tacking [w]tabbed [e]split";
systemMode = "system: [l]ock [e]xit [s]leep [r]eboot [Shift+s]shutdown";
in
# 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, ... }:
{
wayland.windowManager.sway = {
enable = true;
@@ -47,99 +19,15 @@ in
# Launcher: sway-launcher-desktop running inside a floating foot window.
menu = "${pkgs.foot}/bin/foot --app-id=launcher ${pkgs.sway-launcher-desktop}/bin/sway-launcher-desktop";
# Dvorak is a variant of the "us" layout, not a standalone layout --
# `xkb_layout = "dvorak"` fails to compile (no symbols/dvorak) and wlroots
# silently falls back to QWERTY. Use the variant.
input."type:keyboard" = {
xkb_layout = "us";
xkb_variant = "dvorak";
};
# Touchpads (laptops): tap-to-click and natural scrolling. Inert on the
# desktop hosts, which have no touchpad.
input."type:touchpad" = {
tap = "enabled";
natural_scroll = "enabled";
};
# Solid Catppuccin Mocha base as the wallpaper (no image dependency).
output."*".bg = "#${ctp.base} solid_color";
# Window borders -- Catppuccin Mocha (blue accent on the focused window).
colors = {
focused = {
border = "#${ctp.blue}";
background = "#${ctp.base}";
text = "#${ctp.text}";
indicator = "#${ctp.blue}";
childBorder = "#${ctp.blue}";
};
focusedInactive = {
border = "#${ctp.surface0}";
background = "#${ctp.base}";
text = "#${ctp.subtext0}";
indicator = "#${ctp.surface0}";
childBorder = "#${ctp.surface0}";
};
unfocused = {
border = "#${ctp.surface0}";
background = "#${ctp.base}";
text = "#${ctp.subtext0}";
indicator = "#${ctp.surface0}";
childBorder = "#${ctp.surface0}";
};
urgent = {
border = "#${ctp.red}";
background = "#${ctp.base}";
text = "#${ctp.text}";
indicator = "#${ctp.red}";
childBorder = "#${ctp.red}";
};
};
input."type:keyboard".xkb_layout = "dvorak";
window.commands = [
{
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
# returns to the default mode. mkOptionDefault-merged with the module's
# built-in "resize" mode.
modes = {
# Layout submenu (Mod+y). Mirrors Sway's default s/w/e layout keys --
# notably it restores split-toggle, which moved off Mod+e when that
# became the nemo launcher.
${layoutMode} = {
"s" = "layout stacking, mode default";
"w" = "layout tabbed, mode default";
"e" = "layout toggle split, mode default";
"Return" = "mode default";
"Escape" = "mode default";
};
# Power menu (Mod+Shift+x). Lock reuses the themed swaylock; the rest go
# through systemd/logind (allowed for the active local session).
${systemMode} = {
"l" = "exec ${pkgs.swaylock}/bin/swaylock -f, mode default";
"e" = "exec ${pkgs.sway}/bin/swaymsg exit, mode default";
"s" = "exec systemctl suspend, mode default";
"r" = "exec systemctl reboot, mode default";
"Shift+s" = "exec systemctl poweroff, mode default";
"Return" = "mode default";
"Escape" = "mode default";
};
};
bars = [
{
position = "top";
@@ -151,262 +39,28 @@ in
];
size = 11.0;
};
# Bar background + workspace buttons -- Catppuccin Mocha. The mantle
# background matches i3status-rust's idle_bg below for a uniform strip.
colors = {
background = "#${ctp.mantle}";
statusline = "#${ctp.text}";
separator = "#${ctp.surface0}";
focusedWorkspace = {
border = "#${ctp.blue}";
background = "#${ctp.blue}";
text = "#${ctp.base}";
};
activeWorkspace = {
border = "#${ctp.surface0}";
background = "#${ctp.surface0}";
text = "#${ctp.text}";
};
inactiveWorkspace = {
border = "#${ctp.mantle}";
background = "#${ctp.mantle}";
text = "#${ctp.subtext0}";
};
urgentWorkspace = {
border = "#${ctp.red}";
background = "#${ctp.red}";
text = "#${ctp.base}";
};
};
}
];
# NB: this whole set is wrapped in mkOptionDefault so it MERGES with the
# home-manager module's default keybindings (same priority) rather than
# replacing them. Do not wrap it in mkMerge with a normal-priority attr --
# that makes the normal-priority def win and silently drops every default
# bind (terminal, movement, workspaces, ...).
keybindings = lib.mkOptionDefault (
{
# Launcher on Mod+Space. mkForce overrides the module's own default
# Mod+Space (focus mode_toggle); a plain value would conflict with it
# at equal priority. Mod+d also still runs the launcher (module default).
"${modifier}+space" = lib.mkForce "exec ${menu}";
# File manager. mkForce overrides the module default (layout toggle split).
"${modifier}+e" = lib.mkForce "exec ${pkgs.nemo}/bin/nemo";
"${modifier}+l" = "exec ${pkgs.swaylock}/bin/swaylock -f";
# Cycle workspaces.
"${modifier}+z" = "workspace prev";
"${modifier}+x" = "workspace next";
# focus mode_toggle (tiling <-> floating focus) -- re-homed off
# Mod+Space, which is now the launcher.
"${modifier}+Mod1+space" = "focus mode_toggle";
# Enter the binding-mode submenus defined above.
"${modifier}+y" = "mode \"${layoutMode}\"";
"${modifier}+Shift+x" = "mode \"${systemMode}\"";
# Clipboard history: pick a past entry through fuzzel (clipman stores
# it -- see services.clipman below).
"${modifier}+c" =
"exec ${pkgs.clipman}/bin/clipman pick -t CUSTOM --tool-args=\"${pkgs.fuzzel}/bin/fuzzel --dmenu\"";
# Screenshots -> swappy editor: Print = drag a region, Shift+Print =
# the focused window.
"Print" =
"exec ${pkgs.grim}/bin/grim -g \"$(${pkgs.slurp}/bin/slurp)\" - | ${pkgs.swappy}/bin/swappy -f -";
"Shift+Print" = "exec ${screenshotWindow}";
"XF86AudioRaiseVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+";
"XF86AudioLowerVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-";
"XF86AudioMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
"XF86AudioMicMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle";
# Media keys (MPRIS via playerctl).
"XF86AudioPlay" = "exec ${pkgs.playerctl}/bin/playerctl play-pause";
"XF86AudioNext" = "exec ${pkgs.playerctl}/bin/playerctl next";
"XF86AudioPrev" = "exec ${pkgs.playerctl}/bin/playerctl previous";
}
# Screen backlight: laptops only (no internal backlight on a desktop).
// lib.optionalAttrs portable {
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%+";
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
}
);
};
};
# Terminal: Catppuccin Mocha. foot reads ~/.config/foot/foot.ini; the Sway
# `terminal` above still launches the same binary, now themed.
programs.foot = {
enable = true;
# foot 1.27: the bare [colors] section is deprecated in favour of
# [colors-dark] (the default theme), and the cursor colour moved out of
# [cursor] (where `color` is now rejected) into a `cursor` key here, written
# "<text> <cursor>" (man foot.ini(5): "ff0000 00ff00" => green cursor, red
# text). Only colors-dark is needed; we never set initial-color-theme=light.
settings = {
main = {
# Nerd Font: monospace plus the powerline/Nerd glyphs the tmux
# statusline uses (otherwise they render as blank/"?").
font = "JetBrainsMono Nerd Font:size=11";
# Advertise as xterm-256color so remote hosts without foot's terminfo
# still behave (tmux re-adds foot's RGB/sync/etc. features -- see
# shell.nix). The [main] section is the unheadered top of foot.ini.
term = "xterm-256color";
};
scrollback.lines = 100000;
"colors-dark" = {
background = ctp.base;
foreground = ctp.text;
regular0 = ctp.surface1;
regular1 = ctp.red;
regular2 = ctp.green;
regular3 = ctp.yellow;
regular4 = ctp.blue;
regular5 = ctp.pink;
regular6 = ctp.teal;
regular7 = ctp.subtext1;
bright0 = ctp.surface2;
bright1 = ctp.red;
bright2 = ctp.green;
bright3 = ctp.yellow;
bright4 = ctp.blue;
bright5 = ctp.pink;
bright6 = ctp.teal;
bright7 = ctp.subtext0;
"selection-foreground" = ctp.base;
"selection-background" = ctp.rosewater;
cursor = "${ctp.base} ${ctp.rosewater}";
};
};
};
# Clipboard history: a user service runs `wl-paste --watch clipman store`,
# bound to the Wayland session, so copies persist and Mod+c (above) can pick
# 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 = {
enable = true;
settings = {
main = {
font = "Noto Sans:size=12";
prompt = "\"clipboard \"";
};
border = {
width = 2;
radius = 8;
};
colors = {
background = "${ctp.base}f0";
text = "${ctp.text}ff";
prompt = "${ctp.subtext0}ff";
input = "${ctp.text}ff";
match = "${ctp.blue}ff";
selection = "${ctp.surface1}ff";
selection-text = "${ctp.text}ff";
selection-match = "${ctp.blue}ff";
border = "${ctp.blue}ff";
keybindings = lib.mkOptionDefault {
"${modifier}+l" = "exec ${pkgs.swaylock}/bin/swaylock -f";
"Print" = "exec ${pkgs.grim}/bin/grim ~/screenshot-$(date +%F-%H%M%S).png";
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%+";
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
"XF86AudioRaiseVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+";
"XF86AudioLowerVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-";
"XF86AudioMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
};
};
programs.swaylock = {
enable = true;
# Catppuccin Mocha (swaylock colours are hex without "#").
settings = {
color = ctp.base;
color = "1e1e2e";
indicator-radius = 100;
indicator-thickness = 7;
show-failed-attempts = true;
font = "Noto Sans";
inside-color = ctp.base;
inside-clear-color = ctp.base;
inside-ver-color = ctp.base;
inside-wrong-color = ctp.base;
ring-color = ctp.surface1;
ring-clear-color = ctp.yellow;
ring-ver-color = ctp.blue;
ring-wrong-color = ctp.red;
key-hl-color = ctp.blue;
bs-hl-color = ctp.red;
line-color = ctp.base;
line-clear-color = ctp.base;
line-ver-color = ctp.base;
line-wrong-color = ctp.base;
separator-color = "00000000";
text-color = ctp.text;
text-clear-color = ctp.text;
text-ver-color = ctp.text;
text-wrong-color = ctp.text;
};
};
@@ -432,30 +86,16 @@ in
services.dunst = {
enable = true;
# Catppuccin Mocha notifications (dunst colours need a leading "#").
settings = {
global = {
font = "Noto Sans 11";
frame_color = "#${ctp.blue}";
frame_width = 2;
frame_color = "#89b4fa";
separator_color = "frame";
offset = "10x10";
corner_radius = 5;
};
urgency_low = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.surface1}";
};
urgency_normal = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.blue}";
};
urgency_critical = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.peach}";
frame_color = "#fab387";
timeout = 0;
};
};
@@ -464,29 +104,8 @@ in
programs.i3status-rust = {
enable = true;
bars.default = {
# Catppuccin Mocha: a flat "plain" base recoloured via overrides. Idle
# blocks sit on mantle (matching the Sway bar background) with light text;
# only warning/critical states get a loud tinted background. The `theme`
# bar option is shallow-merged away by `settings.theme`, so set the base
# theme and its overrides together here.
theme = "gruvbox-dark";
icons = "awesome6";
settings.theme = {
theme = "plain";
overrides = {
idle_bg = "#${ctp.mantle}";
idle_fg = "#${ctp.text}";
info_bg = "#${ctp.mantle}";
info_fg = "#${ctp.blue}";
good_bg = "#${ctp.mantle}";
good_fg = "#${ctp.green}";
warning_bg = "#${ctp.peach}";
warning_fg = "#${ctp.base}";
critical_bg = "#${ctp.red}";
critical_fg = "#${ctp.base}";
separator_bg = "#${ctp.mantle}";
separator_fg = "#${ctp.surface1}";
};
};
blocks = [
{
block = "disk_space";
@@ -501,20 +120,8 @@ in
block = "cpu";
interval = 2;
}
]
# Desktop-only: CPU temperature and wired network throughput, in place
# of the laptop's battery readout.
++ lib.optionals (!portable) [
{
block = "temperature";
interval = 5;
format = " $icon $average avg, $max max ";
}
{ block = "net"; }
]
++ [ { block = "sound"; } ]
++ lib.optional portable { block = "battery"; }
++ [
{ block = "sound"; }
{ block = "battery"; }
{
block = "time";
interval = 5;
-76
View File
@@ -1,76 +0,0 @@
# Home-manager module for the work (EDaaS/WSL) profile: corporate git signing,
# work toolchain packages and tmux tweaks. Imported only by the work host.
{ 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;
programs.git = {
settings = {
commit.gpgsign = true;
tag.gpgsign = true;
gpg.format = "ssh";
user.signingkey = "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAJMVgeRKnfX1G8coU3nAobI485aeUpGTMqH7+zbKI8o emma.thorpe@cloud.com";
user.email = "emma.thorpe@citrix.com";
};
};
home.packages = [
pkgs.kubectl
pkgs.argo-rollouts
pkgs.tenv
pkgs.kubernetes-helm
pkgs.azure-cli
pkgs.kubelogin
pkgs.curl
pkgs.notation
pkgs.powershell
pkgs.nuget
pkgs.gedit
pkgs.lens
pkgs.python3
pkgs.gnumake
pkgs.gcc
pkgs.libiconv
pkgs.autoconf
pkgs.automake
pkgs.pkg-config
pkgs.wget
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 = {
docker = "/run/current-system/sw/bin/docker";
};
programs.tmux = {
extraConfig = ''
set -g status-right "#(/run/current-system/sw/bin/bash $HOME/code/kube-tmux/kube.tmux 250 red black)"
'';
};
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;
};
}
+16 -119
View File
@@ -1,46 +1,41 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.features.swayDesktop;
# Catppuccin Mocha (shared with the Sway desktop, see lyrathorpe/home/sway.nix).
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;
wrapperFeatures.gtk = true;
extraSessionCommands = ''
# QT
export QT_QPA_PLATFORM="wayland;xcb"
export QT_QPA_PLATFORMTHEME=qt5ct
# SDL
export SDL_VIDEODRIVER=wayland
# Java
export _JAVA_AWT_WM_NONREPARENTING=1
# Misc
export CLUTTER_BACKEND=wayland
export WINIT_UNIX_BACKEND=wayland
export MOZ_ENABLE_WAYLAND=1
'';
# QT
export QT_QPA_PLATFORM="wayland;xcb"
export QT_QPA_PLATFORMTHEME=qt5ct
# SDL
export SDL_VIDEODRIVER=wayland
# Java
export _JAVA_AWT_WM_NONREPARENTING=1
# Misc
export CLUTTER_BACKEND=wayland
export WINIT_UNIX_BACKEND=x11
export MOZ_ENABLE_WAYLAND=1
'';
# Core Wayland utilities. The lock screen, idle daemon, status bar and
# notification daemon are configured per-user in home/sway.nix.
extraPackages = with pkgs; [
brightnessctl
foot
grim
slurp # region selection for screenshots
swappy # screenshot annotation/save
jq # used by the focused-window screenshot bind
playerctl # MPRIS media keys
sway-launcher-desktop
pavucontrol
];
@@ -51,104 +46,6 @@ in
font-awesome
];
# Wayland login screen (replaces console/getty login on every Sway host).
# greetd runs ReGreet inside the cage kiosk compositor; the Sway session is
# offered automatically because programs.sway registers itself via
# services.displayManager.sessionPackages. Hosts that turn off
# features.swayDesktop (e.g. EDaaS) keep plain TTY login.
programs.regreet.enable = true;
# Theme the greeter to match the Sway desktop (Catppuccin Mocha). ReGreet is
# GTK; recolour via CSS (covering both libadwaita named colours and plain
# GTK node selectors) and use the same Noto Sans as the bar/notifications.
programs.regreet.font = {
name = "Noto Sans";
package = pkgs.noto-fonts;
size = 16;
};
programs.regreet.extraCss = ''
/* GTK4 Adwaita legacy names (what plain GTK4 actually references). */
@define-color theme_bg_color #${ctp.base};
@define-color theme_fg_color #${ctp.text};
@define-color theme_base_color #${ctp.mantle};
@define-color theme_text_color #${ctp.text};
@define-color theme_selected_bg_color #${ctp.blue};
@define-color theme_selected_fg_color #${ctp.base};
@define-color insensitive_bg_color #${ctp.mantle};
@define-color insensitive_fg_color #${ctp.overlay0};
@define-color borders #${ctp.surface1};
@define-color warning_color #${ctp.peach};
@define-color error_color #${ctp.red};
@define-color success_color #${ctp.green};
/* libadwaita names (inert on plain GTK4, kept for forward-compat). */
@define-color window_bg_color #${ctp.base};
@define-color window_fg_color #${ctp.text};
@define-color view_bg_color #${ctp.mantle};
@define-color view_fg_color #${ctp.text};
@define-color card_bg_color #${ctp.surface0};
@define-color card_fg_color #${ctp.text};
@define-color accent_bg_color #${ctp.blue};
@define-color accent_fg_color #${ctp.base};
@define-color accent_color #${ctp.blue};
@define-color destructive_bg_color #${ctp.red};
@define-color destructive_fg_color #${ctp.base};
window {
background-color: #${ctp.base};
color: #${ctp.text};
}
label {
color: #${ctp.text};
}
entry {
background-color: #${ctp.surface0};
color: #${ctp.text};
border: 1px solid #${ctp.surface1};
}
entry:focus-within {
border-color: #${ctp.blue};
}
button,
combobox button {
background-color: #${ctp.surface0};
color: #${ctp.text};
border: 1px solid #${ctp.surface1};
}
button:hover {
background-color: #${ctp.surface1};
}
button:active,
button:checked {
background-color: #${ctp.blue};
color: #${ctp.base};
}
'';
# cage reads the XKB_* environment at startup, so force the greeter onto the
# same Dvorak layout as the Sway session (home/sway.nix) -- otherwise the
# password field would be QWERTY. Dvorak is the "us" layout's variant, NOT a
# layout of its own: "dvorak" alone has no symbols/ file, so the keymap
# fails to compile and the greeter ends up with no keyboard at all (the
# greeter has no fallback, unlike a running Sway session). This overrides the
# greetd command regreet sets with mkDefault.
services.greetd.settings.default_session.command =
let
greeter = pkgs.writeShellScript "regreet-cage" ''
export XKB_DEFAULT_LAYOUT=us
export XKB_DEFAULT_VARIANT=dvorak
# ReGreet is plain GTK4 (no libadwaita); force the dark Adwaita variant
# so the extraCss accents sit on a dark base instead of light Adwaita.
export GTK_THEME=Adwaita:dark
exec ${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -- ${lib.getExe config.programs.regreet.package}
'';
in
"${greeter}";
# Desktop portals: enables screen sharing (wlroots) and native file pickers
# for Wayland apps such as Element and Firefox.
xdg.portal = {
+1
View File
@@ -1,6 +1,7 @@
{
config,
pkgs,
inputs,
lib,
username,
fullName,
-8
View File
@@ -1,8 +0,0 @@
# statix lint config. Two default lints are disabled because they flag this
# repo's intentional house style, not bugs:
# repeated_keys - we use `foo.a = ...; foo.b = ...;` (dotted) over nesting.
# empty_pattern - module files use `{ ... }:` / `{ }:` deliberately.
disabled = [
"repeated_keys",
"empty_pattern",
]
-184
View File
@@ -1,184 +0,0 @@
# Default nix-darwin host. Minimal macOS baseline; the user environment
# (shell, git, editor) is carried by the shared ./lyrathorpe/home modules,
# the same ones used by the Linux hosts. nixpkgs.hostPlatform is set by
# mkDarwinHost in flake.nix.
{ pkgs, username, ... }:
{
programs.zsh.enable = true;
# Install the Nerd Font into /Library/Fonts so iTerm2 can use it (set it in
# iTerm2 -> Settings -> Profiles -> Text -> Font: "JetBrainsMono Nerd Font").
# Provides the powerline/Nerd glyphs the tmux statusline draws.
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
# CLI tooling sourced from nixpkgs instead of Homebrew formulae. Pure library
# dependencies are omitted; nix pulls them into closures automatically.
environment.systemPackages = with pkgs; [
# Build & toolchain
autoconf
automake
cmake
coreutils
gcc
gettext
gnumake
pkgconf
ruby
zig
# Version control & dev workflow
pre-commit
deno
opentofu
# Compression
lz4
p7zip
xz
zstd
# Crypto & networking
gnupg
gnutls
openssl
pinentry_mac
unbound
wget
# Media
ffmpeg
svt-av1
yt-dlp
# Graphics / Vulkan / SDL
glslang
moltenvk
spirv-tools
vulkan-loader
vulkan-tools
SDL2
sdl3
# Embedded
esptool
picotool
# Misc utilities
f3
gnused
lua5_4
magic-wormhole
ncurses
mas
sqlite
];
# Account that runs user-level activation and Homebrew.
system.primaryUser = username;
# nix-homebrew owns and installs the Homebrew prefix declaratively, so brew
# itself no longer needs a manual bootstrap. enableRosetta permits x86_64
# formulae via Rosetta 2 on Apple Silicon.
nix-homebrew = {
autoMigrate = true;
enable = true;
enableRosetta = true;
user = username;
};
# Declarative Homebrew for packages with no nixpkgs equivalent or that must be
# the vendor build (GUI casks).
homebrew = {
enable = true;
onActivation = {
autoUpdate = true;
upgrade = true;
# Lists below are authoritative: anything not declared is uninstalled.
cleanup = "zap";
};
taps = [ ];
# Formulae kept on brew: vendor PWA host and version-pinned toolchains that
# are simpler to track via brew than to match exactly in nixpkgs.
brews = [
"firefoxpwa"
"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.
casks = [
"alfred"
"android-platform-tools"
"angry-ip-scanner"
"arduino-ide"
"autodesk-fusion"
"bambu-studio"
"bitwarden"
"citrix-workspace"
"curseforge"
"discord"
"firefox"
"freecad"
"gcc-arm-embedded"
"google-chrome"
"istat-menus"
"iterm2"
"macfuse"
"microsoft-teams"
"nextcloud"
"obs"
"omnidisksweeper"
"openscad@snapshot"
"orcaslicer"
"plex"
"plexamp"
"postman"
"signal"
"steam"
"thunderbird"
"virtualbox"
"visual-studio-code"
"vnc-viewer"
"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;
};
};
# Used for backwards compatibility; read `darwin-rebuild changelog` before changing.
system.stateVersion = 5;
}
+2 -24
View File
@@ -18,8 +18,7 @@
enable = true;
defaultUser = "emmathorpe";
wslConf.automount.root = "/mnt";
wslConf.interop.appendWindowsPath = true;
wslConf.interop.enabled = true;
wslConf.interop.appendWindowsPath = false;
wslConf.network.generateHosts = false;
startMenuLaunchers = true;
docker-desktop.enable = false;
@@ -41,34 +40,13 @@
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.
# The NixOS-WSL store is a read-only VHD whose files are owned by nobody
# (65534), not root. OpenSSH permission-checks Include'd config files and
# rejects any not owned by root or the caller, so the default include fails
# with "Bad owner or permissions" and breaks ssh/git for every command. The
# proxy plugin only matters for `ssh unix/…` / `vsock` to local machined VMs,
# which WSL does not use.
programs.ssh.systemd-ssh-proxy.enable = false;
## patch the script
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
+119 -21
View File
@@ -1,37 +1,94 @@
# MacBook Pro (Apple Silicon, Asahi NixOS). Shared laptop options live in
# ../../modules/laptop.nix; only host-specific settings are here.
{ pkgs, ... }:
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{
config,
lib,
pkgs,
...
}:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# UEFI boot via systemd-boot. Asahi manages the EFI vars from macOS, so do not
# touch them from NixOS.
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = false;
networking.hostName = "Lyra-Asahi";
networking.hostName = "Emma-Asahi"; # Define your hostname.
# Pick only one of the below networking options.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Audio (PipeWire) and the swaylock PAM stack are inherited from
# workstation.nix. hardware.enableRedistributableFirmware is also set there;
# it is harmless here since Asahi supplies its own peripheral firmware below.
# Binary cache for the Asahi kernel/build artifacts, so the MBP pulls prebuilt
# outputs instead of compiling the Asahi kernel locally.
nix.settings = {
substituters = [ "https://nixos-apple-silicon.cachix.org" ];
trusted-public-keys = [
"nixos-apple-silicon.cachix.org-1:8psDu5SA5dAD7qA0zMy5UT292TxeEPzIz8VVEr2Js20="
];
networking.wireless.iwd = {
enable = true;
settings.General.EnableNetworkConfiguration = true;
};
# Apple peripheral firmware (Wi-Fi/Bluetooth). The directory is gitignored and
# populated out-of-band -- see README.
# Set your time zone.
time.timeZone = "Europe/London";
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Select internationalisation properties.
i18n.defaultLocale = "en_GB.UTF-8";
console = {
# font = "Lat2-Terminus16";
keyMap = "dvorak";
# useXkbConfig = true; # use xkb.options in tty.
};
# Enable the X11 windowing system.
# services.xserver.enable = true;
features.swayDesktop.enable = true;
# Allow swaylock to authenticate (no fingerprint reader on this machine).
security.pam.services.swaylock = { };
# Specify path to peripheral firmware files.
hardware.asahi.peripheralFirmwareDirectory = ../../modules/firmware;
# Configure keymap in X11
# services.xserver.xkb.layout = "us";
# services.xserver.xkb.options = "eurosign:e,caps:escape";
# Enable CUPS to print documents.
# services.printing.enable = true;
# Enable sound.
# services.pulseaudio.enable = true;
# OR
# services.pipewire = {
# enable = true;
# pulse.enable = true;
# };
# Enable touchpad support (enabled default in most desktopManager).
# services.libinput.enable = true;
# Define a user account. Don't forget to set a password with passwd.
# users.users.alice = {
# isNormalUser = true;
# extraGroups = [ "wheel" ]; # Enable sudo for the user.
# packages = with pkgs; [
# tree
# ];
# };
# programs.firefox.enable = true;
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
# wget
git
asahi-bless
asahi-nvram
asahi-btsync
@@ -41,6 +98,47 @@
iptables
];
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "25.05";
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
#system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment?
}
-61
View File
@@ -1,61 +0,0 @@
# Mac Pro 3,1 (Early 2008) — install notes
Flake host: `lyrathorpe-macpro31`. Desktop (`portable = false`, imports
`../../modules/desktop.nix`). Files: `configuration.nix`,
`hardware-configuration.nix`.
## Hardware configuration
`hardware-configuration.nix` here is the real config generated by
`nixos-generate-config` on the machine. Root is an **LVM** logical volume
(`/dev/mapper/MacPro-Root`, ext4); the ESP (vfat) and swap are referenced by
UUID. The initrd carries `dm-snapshot` for the LVM root. Regenerate and commit
if the disk layout changes.
## Bootloader
The Mac Pro 3,1 has **64-bit EFI**, so it uses **systemd-boot** (no GRUB/CSM
shim). `canTouchEfiVariables = false` because Apple's firmware does not reliably
accept `efibootmgr` NVRAM writes.
Apple-EFI quirk: if the firmware boot picker does not show NixOS after install,
either
- uncomment `boot.loader.efi.efiInstallAsRemovable = true;` in
`configuration.nix` (installs the fallback `\EFI\BOOT\BOOTX64.EFI`), and/or
- "bless" the ESP from macOS.
Partition the disk GPT with an ESP (vfat).
## Graphics
The stock card varies between units — **ATI Radeon HD 2600 XT** or **NVIDIA
GeForce 8800 GT**. No proprietary driver is hardcoded; Sway relies on in-tree KMS:
- ATI Radeon HD 2600 XT → `radeon` (or `amdgpu`) KMS
- NVIDIA GeForce 8800 GT → `nouveau` KMS
These come up automatically. If a card needs forcing, set
`services.xserver.videoDrivers` and/or add the module to
`boot.initrd.kernelModules` for early KMS (see the comment in
`configuration.nix`).
## Networking
Wired Ethernet via NetworkManager (from `desktop.nix`) — the Mac Pro has two
gigabit ports.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
authenticate. Requires working KMS (radeon/nouveau — see Graphics).
## Apply
```sh
sudo nixos-rebuild switch --flake .#lyrathorpe-macpro31
```
-52
View File
@@ -1,52 +0,0 @@
# Apple Mac Pro 3,1 (Early 2008, dual Xeon Harpertown, x86_64). Desktop host:
# shared graphical/wired options live in ../../modules/desktop.nix; only
# host-specific settings are here. Install notes (EFI booting, GPU, partitions):
# see ./README.md.
{ ... }:
{
imports = [
./hardware-configuration.nix
];
# The Mac Pro 3,1 has 64-bit EFI (confirmed by the owner), so boot via
# systemd-boot like the MBP -- no GRUB/BIOS shim needed.
boot.loader.systemd-boot.enable = true;
# Apple's EFI does not reliably support efibootmgr NVRAM writes; leave the
# firmware vars untouched.
boot.loader.efi.canTouchEfiVariables = false;
# Apple-EFI quirk: if the Mac does not pick up the bootloader at the boot
# picker, install it to the fallback path \EFI\BOOT\BOOTX64.EFI and/or
# "bless" the ESP from macOS. Uncomment to write the removable fallback path:
# boot.loader.efi.efiInstallAsRemovable = true;
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;
networking.firewall.allowedTCPPorts = [ 22 ];
# Dual Harpertown Xeon microcode. Redistributable firmware (GPU/NIC blobs) is
# enabled in workstation.nix.
hardware.cpu.intel.updateMicrocode = true;
# GPU note: the stock card varies between units -- ATI Radeon HD 2600 XT or
# NVIDIA GeForce 8800 GT. Sway needs a working KMS/modesetting driver; do NOT
# install a proprietary blob here. Depending on the installed card, rely on
# the open kernel driver:
# - ATI Radeon HD 2600 XT -> "radeon" (older) or "amdgpu" KMS
# - NVIDIA GeForce 8800 GT -> "nouveau" KMS
# These come up automatically via the in-tree drivers + KMS, and the graphics
# stack itself is enabled by swaywm.nix. If a card needs to be forced, add it
# here, e.g. `services.xserver.videoDrivers = [ "radeon" ];` (or "nouveau"),
# and/or `boot.initrd.kernelModules = [ "radeon" ];` in
# hardware-configuration.nix for early KMS.
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
}
@@ -1,33 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ata_piix" "ahci" "firewire_ohci" "usb_storage" "usbhid" "sd_mod" ];
boot.initrd.kernelModules = [ "dm-snapshot" ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/mapper/MacPro-Root";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/0E9C-C099";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices =
[ { device = "/dev/disk/by-uuid/dec138b4-320f-4b69-acbc-3014a1032cbd"; }
];
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
-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
];
}
-48
View File
@@ -1,48 +0,0 @@
# ThinkPad T400 — install notes
Flake host: `lyrathorpe-t400`. Files: `configuration.nix`, the `boot-*.nix`
variants, and `hardware-configuration.nix`.
## Hardware configuration
`hardware-configuration.nix` here is a hand-written **placeholder**. On the real
machine, run `nixos-generate-config`, replace the file, and commit it. It assumes
by-label partitions — root `nixos` (ext4) and `swap` — so either label them at
install time or swap in the generated UUIDs.
## Bootloader — import the module matching the flashed firmware
`configuration.nix` imports exactly one boot module. Default is `boot-bios.nix`;
switch by commenting it out and uncommenting the relevant alternative.
| Firmware | Module | Notes |
| ---------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stock Lenovo BIOS, or coreboot + **SeaBIOS** payload | `boot-bios.nix` | GRUB on the MBR. Set `device` to the real install disk (`/dev/sda` by default). MBR/legacy layout. |
| coreboot + **GRUB** payload | `boot-coreboot-grub.nix` | GRUB is config-only (`device = "nodev"`); NixOS does **not** write to a disk. Your coreboot `grub.cfg` (in the flash chip) must `search` for and `configfile` the on-disk `/boot/grub/grub.cfg`, or chainload the disk's GRUB. |
| coreboot + **Tianocore/edk2 (UEFI)** payload | `boot-coreboot-uefi.nix` | systemd-boot. `canTouchEfiVariables = true` (coreboot honours NVRAM writes). The module **declares its own ESP** (`/boot` vfat, label `ESP`) — when you regenerate `hardware-configuration.nix`, do **not** let it also define `/boot`. Create + label an `ESP` vfat partition (GPT). |
## Graphics
This unit has the optional **discrete ATI Mobility Radeon HD 3470 (RV620)**. The
open `radeon` KMS driver is loaded in the initrd for early modesetting; firmware
comes from `enableRedistributableFirmware`.
The T400 has switchable graphics (discrete ATI + Intel GMA 4500MHD). Select
**Discrete** in the firmware's graphics setting so only the ATI is live. If you
run **Integrated** instead, the Intel `i915` driver takes over with no config
change and `radeon` stays idle.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
authenticate. Requires working radeon/i915 KMS (see Graphics).
## Apply
```sh
sudo nixos-rebuild switch --flake .#lyrathorpe-t400
```
-11
View File
@@ -1,11 +0,0 @@
# Boot via legacy BIOS -- the stock Lenovo BIOS, or coreboot with the SeaBIOS
# payload (both present a legacy BIOS interface). GRUB is installed to the MBR of
# the boot disk. This is the default.
{ ... }:
{
boot.loader.grub = {
enable = true;
# Must point at the actual install disk -- adjust if it is not /dev/sda.
device = "/dev/sda";
};
}
@@ -1,13 +0,0 @@
# Boot via coreboot's GRUB payload (e.g. libreboot default). The GRUB in the
# flash chip reads the grub.cfg that NixOS generates on disk, so GRUB here is
# config-only -- it is NOT installed to any disk MBR (`device = "nodev"`).
#
# Your coreboot grub.cfg must locate and load the on-disk config, e.g. search
# for and `configfile` /boot/grub/grub.cfg (or chainload the disk's GRUB).
{ ... }:
{
boot.loader.grub = {
enable = true;
device = "nodev";
};
}
@@ -1,17 +0,0 @@
# Boot via coreboot's Tianocore/edk2 (UEFI) payload. This turns the T400 into a
# real UEFI machine, so use systemd-boot. Unlike Apple's firmware, coreboot's
# UEFI honours EFI variable writes, so canTouchEfiVariables is on.
#
# Requires an EFI System Partition. It is declared here so it travels with this
# boot mode; the generated hardware-configuration.nix should NOT also define
# /boot. Label the ESP `ESP` at install, or replace with the generated UUID.
{ ... }:
{
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
fileSystems."/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
}
-57
View File
@@ -1,57 +0,0 @@
# 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 = [
./hardware-configuration.nix
# Boot: import exactly ONE, matching the firmware currently flashed.
# Stock Lenovo BIOS and coreboot+SeaBIOS both use boot-bios.nix.
./boot-bios.nix
# ./boot-coreboot-grub.nix # coreboot with the GRUB payload (config-only GRUB)
# ./boot-coreboot-uefi.nix # coreboot with the Tianocore/edk2 UEFI payload
# # (systemd-boot; carries its own ESP mount)
];
networking.hostName = "T400-NixOS";
console.font = "Lat2-Terminus16";
# Low-RAM host (4 GiB max): a compressed RAM swap reduces disk paging.
zramSwap.enable = true;
# This host accepts SSH, so open 22 (the firewall itself is enabled in
# laptop.nix with a default-deny policy).
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [ 22 ];
# Intel Core 2 (Penryn) microcode. Redistributable firmware (enabled in
# workstation.nix) supplies the iwlwifi blobs (Intel WiFi Link 5100/5300) and
# 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
# enableRedistributableFirmware above.
#
# The T400 has switchable graphics (this discrete GPU + the Intel GMA
# 4500MHD). Select "Discrete" in the firmware's graphics setting so only the
# ATI is live; if you instead run "Integrated", the Intel i915 driver takes
# over with no extra config and `radeon` simply stays idle.
boot.initrd.kernelModules = [ "radeon" ];
# See `man configuration.nix` / the stateVersion docs before changing.
system.stateVersion = "26.05";
}
@@ -1,44 +0,0 @@
# PLACEHOLDER -- hand-written, not machine-generated. Regenerate on the real
# T400 with `nixos-generate-config` and commit the result. The device labels
# below are guesses; replace them with the generated UUIDs (or label the
# partitions accordingly at install time).
{
config,
lib,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"ahci"
"ata_piix"
"ehci_pci"
"uhci_hcd"
"usb_storage"
"sd_mod"
"sr_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
# Label your root partition `nixos` at install, or replace with the generated UUID.
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
swapDevices = [
{ device = "/dev/disk/by-label/swap"; }
];
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+134
View File
@@ -0,0 +1,134 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{
config,
lib,
pkgs,
...
}:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "X1-NixOS"; # Define your hostname.
networking.domain = "client.cbg.emmaisvery.gay";
features.swayDesktop.enable = true;
# Pick only one of the below networking options.
networking.wireless.iwd = {
enable = true;
settings.General.EnableNetworkConfiguration = true;
};
# networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Set your time zone.
time.timeZone = "Europe/London";
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Select internationalisation properties.
i18n.defaultLocale = "en_GB.UTF-8";
console = {
font = "Lat2-Terminus16";
keyMap = "dvorak";
# useXkbConfig = true; # use xkb.options in tty.
};
# Enable the X11 windowing system.
# services.xserver.enable = true;
# Configure keymap in X11
# services.xserver.xkb.layout = "us";
# services.xserver.xkb.options = "eurosign:e,caps:escape";
# Enable CUPS to print documents.
# services.printing.enable = true;
# Enable sound.
# hardware.pulseaudio.enable = true;
# OR
services.pipewire = {
enable = true;
pulse.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.libinput.enable = true;
# Define a user account. Don't forget to set a password with passwd.
# users.users.alice = {
# isNormalUser = true;
# extraGroups = [ "wheel" ]; # Enable sudo for the user.
# packages = with pkgs; [
# tree
# ];
# };
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
git
fastfetch
# wget
];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
#system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "24.11"; # Did you read the comment?
# TODO: Move to fprint security module to import anywhere
services.fprintd.enable = true;
security.pam.services.swaylock = {
fprintAuth = true;
};
}
@@ -0,0 +1,42 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/a7145534-b122-4899-a75a-3d2e78474d6b";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/1338-3D4F";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
swapDevices =
[ { device = "/dev/disk/by-uuid/e553c8dc-9d5a-48ec-87bc-9c86ce5932a4"; }
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp0s20f3.useDHCP = lib.mkDefault true;
# networking.interfaces.wwan0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
-62
View File
@@ -1,62 +0,0 @@
# Options shared by every NixOS host (laptops and the WSL box). Imported via
# baseModules in flake.nix. Host- and platform-specific settings stay in the
# per-machine configs; laptop-only settings live in ./laptop.nix.
{ pkgs, ... }:
{
time.timeZone = "Europe/London";
i18n.defaultLocale = "en_GB.UTF-8";
# Store hygiene. auto-optimise-store hard-links identical files in the store
# after each build (cheap dedupe; NOT a garbage collector -- there is
# deliberately no automatic GC timer). The larger download buffer avoids
# "buffer full" stalls when fetching big NARs over a fast link.
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; [
git
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" ];
};
}
-13
View File
@@ -1,13 +0,0 @@
# Desktop (non-portable) NixOS hosts. Counterpart to ./laptop.nix: imports the
# shared ./workstation.nix base and swaps the mobile Wi-Fi backend for wired
# NetworkManager. A desktop host also sets `portable = false` in its host-table
# entry (flake.nix), which drops the battery block and brightness keybindings
# from the Sway bar -- see lyrathorpe/home/sway.nix.
{ ... }:
{
imports = [ ./workstation.nix ];
# Wired networking. NetworkManager handles DHCP/connections itself; do not
# combine with networking.wireless.* (laptop.nix) -- the two backends conflict.
networking.networkmanager.enable = true;
}
-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";
}
-31
View File
@@ -1,31 +0,0 @@
# Portable NixOS hosts (X1, MBP-Asahi). Imported from the host table in
# flake.nix. Shared graphical-workstation settings live in ./workstation.nix;
# the only laptop-specific bit is the Wi-Fi backend. Mobile home-manager
# components (battery block, brightness keys) are gated by the `portable` flag
# threaded through mkHost -- see lyrathorpe/home/sway.nix.
{ ... }:
{
imports = [ ./workstation.nix ];
# Wi-Fi via iwd with its built-in DHCP/network configuration.
networking.wireless.iwd = {
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"
];
}
+55
View File
@@ -0,0 +1,55 @@
{
config,
pkgs,
inputs,
lib,
...
}:
{
programs.git = {
settings = {
commit.gpgsign = true;
tag.gpgsign = true;
gpg.format = "ssh";
user.signingkey = "key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAJMVgeRKnfX1G8coU3nAobI485aeUpGTMqH7+zbKI8o emma.thorpe@cloud.com";
user.email = "emma.thorpe@citrix.com";
};
};
home.packages = [
pkgs.kubectl
pkgs.argo-rollouts
pkgs.tenv
pkgs.kubernetes-helm
pkgs.azure-cli
pkgs.kubelogin
pkgs.curl
pkgs.notation
pkgs.powershell
pkgs.nuget
pkgs.gedit
pkgs.lens
pkgs.python3
pkgs.gnumake
pkgs.gcc
pkgs.libiconv
pkgs.autoconf
pkgs.automake
pkgs.pkg-config
pkgs.wget
pkgs.claude-code
pkgs.google-cloud-sdk
];
services.ssh-agent.enable = true;
home.shellAliases = {
docker = "/run/current-system/sw/bin/docker";
};
programs.tmux = {
extraConfig = ''
set -g status-right "#(/run/current-system/sw/bin/bash $HOME/code/kube-tmux/kube.tmux 250 red black)"
'';
};
programs.go = {
enable = true;
};
}
-49
View File
@@ -1,49 +0,0 @@
# Form-factor-agnostic base for the physical graphical NixOS machines. Imported
# by both ./laptop.nix and ./desktop.nix; those add only the bits that differ
# between portable and desktop hosts (chiefly the networking backend).
#
# 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;
# Disk hygiene for the physical hosts. fstrim reclaims unused SSD blocks on a
# weekly timer; cleanOnBoot wipes /tmp at every boot.
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 = {
enable = true;
pulse.enable = true;
};
# swaylock PAM stack. None of these machines has working fingerprint auth, so
# an empty service is enough -- swaylock falls back to password.
security.pam.services.swaylock = { };
# Redistributable firmware (GPU/Wi-Fi/NIC blobs) for the x86 hosts. Harmless
# on the Asahi MBP, which supplies its own peripheral firmware out-of-band.
hardware.enableRedistributableFirmware = true;
}