155 Commits

Author SHA1 Message Date
Miguel Palhas 0322a8d9de chore(flake): bump maestro to 8325bc6
CI / lint (push) Successful in 1m43s
CI / eval (push) Successful in 4m22s
2026-09-03 23:05:29 +01:00
Miguel Palhas 21b4f9e30e chore(flake): bump maestro to 8f499db
CI / lint (push) Successful in 1m30s
CI / eval (push) Successful in 4m3s
2026-09-03 22:56:12 +01:00
Miguel Palhas 12105a75d6 chore(flake): bump maestro to f8b434d
CI / lint (push) Successful in 2m17s
CI / eval (push) Successful in 4m34s
2026-09-03 17:49:28 +01:00
Miguel Palhas 108814238a chore(flake): bump maestro to f37ff68
CI / lint (push) Successful in 1m6s
CI / eval (push) Successful in 4m8s
2026-09-03 15:33:16 +01:00
Miguel Palhas b163785d2a chore(flake): bump maestro to 46a000a
CI / lint (push) Successful in 59s
CI / eval (push) Successful in 4m22s
2026-09-03 14:09:09 +01:00
Miguel Palhas dd263b3e9f chore(flake): bump maestro to b0504eb
CI / lint (push) Successful in 1m25s
CI / eval (push) Successful in 6m14s
2026-09-03 13:40:48 +01:00
Miguel Palhas 4c16212e0a chore(flake): bump maestro to 6f0ac2b
CI / lint (push) Successful in 1m29s
CI / eval (push) Successful in 5m46s
2026-09-03 13:38:09 +01:00
Miguel Palhas 5ce2aefc67 chore(flake): bump maestro to f443f62
CI / lint (push) Successful in 1m18s
CI / eval (push) Successful in 3m51s
2026-09-03 13:22:23 +01:00
Miguel Palhas d80b481e1a chore(flake): bump maestro to 71e0e8d
CI / eval (push) Successful in 4m56s
CI / lint (push) Failing after 10m57s
2026-09-03 11:12:23 +01:00
Miguel Palhas b2f5625a63 chore(flake): bump maestro to d8a5d95
CI / lint (push) Successful in 37s
CI / eval (push) Successful in 2m43s
2026-09-03 11:09:34 +01:00
Miguel Palhas 2431722fe7 chore(flake): bump maestro to 05e7293
CI / lint (push) Successful in 1m54s
CI / eval (push) Successful in 4m9s
2026-09-03 10:34:34 +01:00
Miguel Palhas d4ee1c66a8 chore(flake): bump maestro to 7eeb437
CI / eval (push) Failing after 1m14s
CI / lint (push) Successful in 1m42s
2026-09-03 08:28:55 +01:00
Miguel Palhas 826c4b4058 chore(flake): bump maestro to b36d571
CI / eval (push) Failing after 1m7s
CI / lint (push) Successful in 1m37s
2026-09-03 08:15:49 +01:00
Miguel Palhas 6ede2df401 chore(flake): bump maestro to e4601e3
CI / eval (push) Failing after 1m35s
CI / lint (push) Successful in 1m32s
2026-09-02 23:09:31 +01:00
Miguel Palhas 0556b58c79 chore(flake): bump maestro to 86eeae8
CI / eval (push) Failing after 1m11s
CI / lint (push) Successful in 1m25s
2026-09-02 22:19:25 +01:00
Miguel Palhas 47c0732624 chore(flake): bump maestro to f669ebb
CI / eval (push) Failing after 1m37s
CI / lint (push) Successful in 2m26s
2026-09-02 21:54:05 +01:00
Miguel Palhas e14bdcf3d1 chore(flake): bump maestro to b163f24
CI / eval (push) Failing after 1m21s
CI / lint (push) Successful in 1m24s
2026-09-02 21:38:02 +01:00
Miguel Palhas c56ef62cf4 chore(flake): bump maestro to 0babde1
CI / eval (push) Failing after 1m42s
CI / lint (push) Successful in 2m14s
2026-09-02 21:09:48 +01:00
Miguel Palhas 914753fb55 fix(yolo): drop removed services.maestro.web.enable
CI / lint (push) Successful in 31s
CI / eval (push) Failing after 33s
maestro's daemon serves the browser UI on daemon.bind_addr; the option is now
a mkRemovedOptionModule, so every flake bump failed to build. config.toml moves
the daemon to 0.0.0.0:7070 and Traefik's maestro router now points there.
2026-09-02 19:31:38 +01:00
Miguel Palhas 2df744b244 chore(flake): bump maestro to 9dad3e6
CI / eval (push) Failing after 43s
CI / lint (push) Successful in 57s
2026-09-02 18:38:44 +01:00
Miguel Palhas c4db021783 chore(flake): bump maestro to d69107b
CI / eval (push) Failing after 2m22s
CI / lint (push) Successful in 2m31s
2026-09-02 18:18:21 +01:00
Miguel Palhas 0a26367881 chore(flake): bump maestro to fa7d5fb
CI / lint (push) Successful in 1m9s
CI / eval (push) Failing after 2m6s
2026-09-02 18:12:18 +01:00
Miguel Palhas 01de255a7a chore(flake): bump maestro to e18ad71
CI / lint (push) Successful in 1m29s
CI / eval (push) Failing after 1m29s
2026-09-02 18:04:50 +01:00
Miguel Palhas a5661de579 chore(flake): bump maestro to f84199d
CI / eval (push) Failing after 2m26s
CI / lint (push) Successful in 3m5s
2026-09-02 17:54:05 +01:00
Miguel Palhas 1079822f67 chore(flake): bump maestro to 2ad2858
CI / eval (push) Failing after 1m29s
CI / lint (push) Successful in 1m32s
2026-09-02 17:41:04 +01:00
Miguel Palhas eb3eb0fe40 chore(flake): bump maestro to e12baa2
CI / eval (push) Failing after 1m5s
CI / lint (push) Successful in 2m17s
2026-09-02 17:37:22 +01:00
Miguel Palhas 9bd137c31f chore(flake): bump maestro to 63191c7
CI / eval (push) Failing after 1m10s
CI / lint (push) Successful in 1m10s
2026-09-02 17:21:22 +01:00
Miguel Palhas 4088e0f72c chore(flake): bump maestro to c8ab4a5
CI / eval (push) Failing after 1m36s
CI / lint (push) Successful in 1m27s
2026-09-02 17:18:35 +01:00
Miguel Palhas 79a8cae2e7 chore(flake): bump maestro to 21fc20b
CI / lint (push) Successful in 1m9s
CI / eval (push) Failing after 1m18s
2026-09-02 17:01:00 +01:00
Miguel Palhas 0e6109df93 chore(flake): bump maestro to adbf57f
CI / eval (push) Failing after 34s
CI / lint (push) Successful in 1m44s
2026-09-02 16:32:27 +01:00
Miguel Palhas 954d23cce6 chore(flake): bump maestro to 3ce1a6f
CI / eval (push) Failing after 2m17s
CI / lint (push) Successful in 1m11s
2026-09-02 16:01:24 +01:00
Miguel Palhas 6f3e4fb462 chore(flake): bump maestro to 8cb6aeb
CI / eval (push) Failing after 1m2s
CI / lint (push) Successful in 1m18s
2026-09-02 15:58:53 +01:00
Miguel Palhas 35f0145e22 chore(flake): bump maestro to 29545d1
CI / eval (push) Failing after 36s
CI / lint (push) Successful in 2m33s
2026-09-02 15:55:03 +01:00
Miguel Palhas b21b25fe7b feat(yolo): auto-apply maestro flake bumps
CI / eval (push) Failing after 30s
CI / lint (push) Successful in 32s
Sessions survive a maestro daemon restart, so there is no reason to stop at
bumping the lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:36:59 +01:00
Miguel Palhas a50eaa4707 Merge remote-tracking branch 'origin/main' into dorian
CI / eval (push) Failing after 1m53s
CI / lint (push) Successful in 1m12s
2026-09-02 15:26:16 +01:00
Miguel Palhas d74f1ac39e chore(flake): bump maestro to f89be4a
CI / eval (push) Failing after 2m15s
CI / lint (push) Successful in 2m49s
2026-09-02 15:26:01 +01:00
Miguel Palhas ccc5d0d88c Merge remote-tracking branch 'origin/main' into dorian 2026-09-02 15:25:56 +01:00
Miguel Palhas ca1cba782e chore(flake): bump agent-skills to abd2192
CI / lint (push) Successful in 56s
CI / eval (push) Successful in 5m30s
2026-09-02 14:39:34 +01:00
Miguel Palhas 434ed3729f chore(flake): bump claude-code to 2.1.258
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:39:20 +01:00
Miguel Palhas df9fb4c495 chore(flake): bump rev to b432e8a
CI / lint (push) Successful in 1m10s
CI / eval (push) Successful in 8m11s
2026-09-02 14:37:54 +01:00
Miguel Palhas 4b12fd94db chore(flake): bump agent-skills to dda9127
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 4m14s
2026-09-02 14:35:22 +01:00
Miguel Palhas aa7a9b179d fix(smb): move yolo share out of $HOME
CI / lint (push) Successful in 35s
CI / eval (push) Successful in 2m15s
starship and eww stat every entry of the home dir, so the automount at
~/yolo fired on every prompt and blocked 10s while the VPN was still
down after boot.
2026-09-02 09:26:32 +01:00
Miguel Palhas 08db70518b chore(flake): bump agent-skills to 6b3cabe
CI / lint (push) Successful in 36s
CI / eval (push) Successful in 2m16s
2026-09-01 15:39:01 +01:00
Miguel Palhas 09bd517439 chore(flake): bump maestro to 28778b2
CI / lint (push) Successful in 1m58s
CI / eval (push) Successful in 5m41s
2026-09-01 15:19:04 +01:00
Miguel Palhas 2cfdc7e68e feat(konishi): flatpak for PrusaSlicer 3.0 alpha
CI / lint (push) Successful in 36s
CI / eval (push) Successful in 2m38s
Prusa ships no Linux binary as of 3.0 — the GitHub release has only
.dmg and .zip, and Flathub is now the sole Linux channel, with alphas
in flathub-beta.

nixpkgs' services.flatpak only exposes `enable`, so this pulls in
nix-flatpak for declarative remotes and packages.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 15:08:33 +01:00
Miguel Palhas 5bafa07427 chore(flake): bump maestro to 766eabb
CI / lint (push) Successful in 2m34s
CI / eval (push) Successful in 4m32s
2026-09-01 14:31:43 +01:00
Miguel Palhas f8c95e3b8d chore(flake): bump maestro to c67fa2b
CI / lint (push) Successful in 2m21s
CI / eval (push) Successful in 4m35s
2026-09-01 11:52:57 +01:00
Miguel Palhas 00810b4104 chore(flake): bump maestro to 88b6bba
CI / lint (push) Successful in 2m16s
CI / eval (push) Successful in 4m38s
2026-09-01 11:26:59 +01:00
Miguel Palhas ede94f9251 chore(flake): bump maestro to 1c349e5
CI / lint (push) Successful in 1m23s
CI / eval (push) Successful in 3m50s
2026-09-01 10:36:17 +01:00
Miguel Palhas ca0ad8f662 chore(flake): bump maestro to 9fc21b4
CI / lint (push) Successful in 1m9s
CI / eval (push) Successful in 4m24s
2026-09-01 10:01:23 +01:00
Miguel Palhas 0519030103 chore(flake): bump maestro to 19162f9
CI / lint (push) Successful in 1m10s
CI / eval (push) Successful in 4m49s
2026-09-01 09:55:00 +01:00
Miguel Palhas 4cb6e63e55 chore(flake): bump maestro to 95b459b
CI / lint (push) Successful in 2m42s
CI / eval (push) Successful in 44m45s
2026-09-01 09:47:01 +01:00
Miguel Palhas 6581bab9ca chore(flake): bump maestro to 31158cd
CI / lint (push) Successful in 1m22s
CI / eval (push) Successful in 4m9s
2026-08-31 22:39:29 +01:00
Miguel Palhas 7c59ed8af6 chore(flake): bump maestro to 7b04575
CI / lint (push) Successful in 1m14s
CI / eval (push) Successful in 3m43s
2026-08-31 22:25:06 +01:00
Miguel Palhas 2b5c0eabea chore(flake): bump maestro to 52abfc6
CI / lint (push) Successful in 1m34s
CI / eval (push) Successful in 3m43s
2026-08-31 22:14:42 +01:00
Miguel Palhas f3acdf7781 chore(flake): bump maestro to 1f543d1
CI / lint (push) Successful in 1m35s
CI / eval (push) Successful in 3m41s
2026-08-31 22:03:19 +01:00
Miguel Palhas 16bf875f5b chore(flake): bump maestro to f6cfcbb
CI / lint (push) Successful in 1m13s
CI / eval (push) Successful in 3m14s
2026-08-31 17:55:05 +01:00
Miguel Palhas 75f216165c chore(flake): bump maestro to eb5d28d
CI / lint (push) Successful in 51s
CI / eval (push) Successful in 3m3s
2026-08-31 15:32:51 +01:00
Miguel Palhas 89fb6c8b14 chore(flake): bump maestro to a922023
CI / lint (push) Successful in 49s
CI / eval (push) Successful in 5m2s
2026-08-31 15:31:59 +01:00
Miguel Palhas 13eb534ab3 chore(flake): bump maestro to d8d26d1
CI / lint (push) Successful in 52s
CI / eval (push) Successful in 4m23s
2026-08-31 15:25:56 +01:00
Miguel Palhas 803f63d92b chore(flake): bump maestro to aec7f4c
CI / lint (push) Successful in 1m32s
CI / eval (push) Successful in 5m14s
2026-08-31 15:25:44 +01:00
Miguel Palhas 0d43a0178a chore(flake): bump maestro to 1257ec6
CI / lint (push) Successful in 1m26s
CI / eval (push) Successful in 3m29s
2026-08-31 15:20:21 +01:00
Miguel Palhas 5f3f502b8d chore(flake): bump maestro to 2dac56b
CI / lint (push) Successful in 57s
CI / eval (push) Successful in 4m31s
2026-08-31 15:06:26 +01:00
Miguel Palhas 92b51ddffc chore(flake): bump maestro to 5c391f1
CI / lint (push) Successful in 54s
CI / eval (push) Successful in 4m14s
2026-08-31 15:03:01 +01:00
Miguel Palhas cf74a9e49b chore(flake): bump maestro to a503ec7
CI / lint (push) Successful in 1m2s
CI / eval (push) Successful in 4m43s
2026-08-31 14:52:37 +01:00
Miguel Palhas f1cbd1d540 chore(flake): bump maestro to 904a665
CI / lint (push) Successful in 1m0s
CI / eval (push) Successful in 4m17s
2026-08-31 14:43:52 +01:00
Miguel Palhas 448c5df0ed chore(flake): bump maestro to f0851a6
CI / lint (push) Successful in 1m25s
CI / eval (push) Successful in 4m37s
2026-08-31 14:42:20 +01:00
Miguel Palhas 88d0be4098 chore(flake): bump maestro to 5b2add0
CI / lint (push) Successful in 1m26s
CI / eval (push) Failing after 2m56s
2026-08-31 09:59:14 +01:00
Miguel Palhas 755de4fb71 chore(flake): bump maestro to d101574
CI / eval (push) Failing after 2m12s
CI / lint (push) Successful in 47s
2026-08-31 09:25:46 +01:00
Miguel Palhas 7d0018903d chore(flake): bump maestro to 8110512
CI / lint (push) Successful in 1m12s
CI / eval (push) Failing after 1m47s
2026-08-31 09:11:13 +01:00
Miguel Palhas 799158d90e chore(flake): bump maestro to d032036
CI / lint (push) Successful in 55s
CI / eval (push) Failing after 1m40s
2026-08-31 08:43:07 +01:00
Miguel Palhas 5da1b61309 chore(flake): bump maestro to 43b90cc
CI / lint (push) Successful in 3m13s
CI / eval (push) Successful in 5m24s
2026-08-30 11:35:17 +01:00
Miguel Palhas 7a5c9981a7 chore(flake): bump agent-skills to d1a198a
CI / lint (push) Successful in 28s
CI / eval (push) Successful in 2m16s
2026-08-28 19:40:51 +01:00
Miguel Palhas 0d364dcbbb chore(flake): bump agent-skills to aff0e38
CI / lint (push) Successful in 34s
CI / eval (push) Successful in 2m14s
2026-08-27 15:54:02 +01:00
Miguel Palhas 8d39467f6b chore(flake): bump agent-skills to ca02107
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 2m4s
2026-08-26 10:59:08 +01:00
Miguel Palhas e56962960b fix(yolo): disable KVM async page faults
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 2m5s
Tasks were wedging in kvm_async_pf_task_wait_schedule and surviving
SIGKILL — maestro-web, systemd activation generators and agent sessions,
a few a day, clearing only on reboot. The host pages this VM's memory out
and a "page ready" wakeup gets dropped, leaving the task in an
uninterruptible wait with no safe point to deliver a signal.

no-kvmapf trades that for a synchronous vCPU stall on host page-in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:06:36 +01:00
Miguel Palhas 25bb46b706 chore(flake): bump agent-skills to a229d0c
CI / lint (push) Successful in 27s
CI / eval (push) Successful in 2m6s
2026-08-26 07:02:06 +01:00
Miguel Palhas 7381a6b764 chore(flake): bump maestro to 999dbe3
CI / lint (push) Successful in 52s
CI / eval (push) Successful in 4m4s
2026-08-25 18:47:20 +01:00
Miguel Palhas 8befa14be1 chore(flake): bump maestro to c9d0895
CI / lint (push) Successful in 2m51s
CI / eval (push) Successful in 5m42s
2026-08-25 18:46:29 +01:00
Miguel Palhas bf85f679f8 chore(maestro): stop nix-managing config.toml
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 2m10s
config.toml is now a hand-edited plain file at ~/.config/maestro,
not a nix-generated symlink.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:08:28 +01:00
Miguel Palhas 6d2754318c Merge remote-tracking branch 'origin/main' into apply
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 2m6s
2026-08-25 16:51:11 +01:00
Miguel Palhas 49579d92dd feat(maestro): set tui theme to dark 2026-08-25 16:51:08 +01:00
Miguel Palhas 7495d84f1e chore(flake): bump maestro to 9ef3547
CI / lint (push) Successful in 2m28s
CI / eval (push) Successful in 4m41s
2026-08-25 15:10:52 +01:00
Miguel Palhas 9e03f255e4 chore(flake): bump maestro to 2ab07c2
CI / eval (push) Successful in 5m49s
CI / lint (push) Failing after 10m39s
2026-08-25 14:22:21 +01:00
Miguel Palhas 3922a93586 chore(flake): bump agent-skills to 8e6f86c
CI / lint (push) Successful in 34s
CI / eval (push) Successful in 1m58s
2026-08-25 14:16:51 +01:00
Miguel Palhas 4a90cd1dfc chore(flake): bump maestro to 39d4014
CI / lint (push) Successful in 1m21s
CI / eval (push) Successful in 4m24s
2026-08-25 12:35:42 +01:00
Miguel Palhas 0039ab20ee chore(flake): bump maestro to c2ac3f7
CI / lint (push) Successful in 42s
CI / eval (push) Successful in 4m25s
2026-08-25 12:26:28 +01:00
Miguel Palhas 5d53b7bf35 chore(flake): bump maestro to 7cb14c7
CI / lint (push) Successful in 2m44s
CI / eval (push) Successful in 4m35s
2026-08-25 10:37:18 +01:00
Miguel Palhas 51a6696efb chore(flake): bump agent-skills to d6265b0
CI / lint (push) Successful in 57s
CI / eval (push) Successful in 3m24s
2026-08-25 10:02:56 +01:00
Miguel Palhas 05bae722f7 feat(yolo): autodeploy agent-skills too
CI / lint (push) Successful in 54s
CI / eval (push) Successful in 3m40s
Same shape as rev: a push to main bumps the input, commits the lock and
switches. The units it ships (pr-daemon, hourlog, week-review) restart without
disturbing anything interactive.
2026-08-25 10:02:55 +01:00
Miguel Palhas 5ee27b6df1 chore(flake): bump rev to 88bf55a
CI / lint (push) Successful in 42s
CI / eval (push) Successful in 3m46s
2026-08-25 10:01:01 +01:00
Miguel Palhas d2c03ed846 Merge remote-tracking branch 'origin/main' into drift
CI / lint (push) Successful in 44s
CI / eval (push) Successful in 2m33s
2026-08-25 09:55:15 +01:00
Miguel Palhas e08e845b38 fix(nix-autodeploy): pass the repo map as a file
systemd's Environment= strips the quotes out of inline JSON, so the listener
died on startup parsing it. A store file has no such problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:55:15 +01:00
Miguel Palhas 91e6b195cf chore(flake): bump maestro to 80a9399
CI / lint (push) Successful in 58s
CI / eval (push) Successful in 3m3s
2026-08-25 09:54:36 +01:00
Miguel Palhas 4d93e94c12 chore(flake): bump agent-skills to 0900e5f
CI / lint (push) Successful in 36s
CI / eval (push) Successful in 3m23s
2026-08-25 09:51:28 +01:00
Miguel Palhas c99627b3f6 feat(yolo): run rev from nix, deploy both apps by webhook
CI / lint (push) Successful in 27s
CI / eval (push) Successful in 3m31s
rev came from a working checkout: the unit ran `node server/index.ts` in
~/tea/rev, so any edit there was live, and rev-deploy rebuilt a prod directory
that no longer exists. It is a flake input now, same as maestro, and
`services.rev` owns the unit.

Adds nix-autodeploy: one endpoint both repos post to. It verifies the webhook
HMAC, bumps that flake input, commits and pushes flake.lock, then switches. The
switch runs in a transient unit, since it restarts the listener itself. maestro
is bump-and-notify only — switching would restart the daemon that owns every
interactive shell here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:58 +01:00
Miguel Palhas 2a3ccd2442 chore(home): drop mutableFiles machinery
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m4s
The aoe config was the last tracked file, and it drifts the same way
settings.json did. With no consumers left, the module, its repo-path option
and the tracked aoe-config.toml go too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 08:29:09 +01:00
Miguel Palhas 2c692550db chore(claude): stop managing settings.json
Claude Code rewrites ~/.claude/settings.json itself, so the nix copy drifted
within every session and `nh home switch` aborted on the diff. Drops the
mutableFiles entries and the two tracked JSON sources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 08:27:39 +01:00
Miguel Palhas a23488dc52 chore(yolo): update maestro input, sync claude/aoe mutable config
CI / lint (push) Successful in 32s
CI / eval (push) Successful in 2m2s
Both mutable-managed files had drifted from what the running apps
actually wrote; bringing the live content upstream unblocked the
home-manager switch that picks up the new maestro build.
2026-08-24 16:49:41 +01:00
Miguel Palhas 2e464d2fe8 chore(konishi): leave blender CUDA off for now
CI / lint (push) Successful in 49s
CI / eval (push) Successful in 2m41s
The option stays as the switch; the build cost is the only reason.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 13:59:00 +01:00
Miguel Palhas 58e953a857 perf(konishi): mount root noatime
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m4s
Set outside hardware-configuration.nix so it survives regeneration.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:13:44 +01:00
Miguel Palhas 014dbde21d perf(konishi): fix GPU env vars for decode
GDM_BACKEND was a typo for GBM_BACKEND; nothing read it, since SDDM is
the display manager here.

Firefox/Zen decode in a separate RDD process whose sandbox denies
/dev/nvidia*, so nvidia-vaapi-driver never initialised there and all
playback fell back to software. Confirmed on the running session: the
GPU process held 26 nvidia fds, both RDD processes held zero.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:13:44 +01:00
Miguel Palhas 48c33fdbb5 perf(zsh): rebuild completion dump once a day
compinit security-checked and rebuilt the dump on every shell start, the
largest fixed cost before fzf-tab, atuin, zoxide, direnv and starship
load. Verified both branches yield the same 1980 completions.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:13:38 +01:00
Miguel Palhas c11acc8b88 feat(blender): optional CUDA build
Off by default: it is only useful on an NVIDIA host, and no binary cache
serves it (cuda-maintainers was measured to change nothing), so enabling
it rebuilds Blender, OpenUSD, OpenSubdiv and OpenImageDenoise.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:13:38 +01:00
Miguel Palhas 3d372cd155 feat(mpv): hardware decode with per-host hwdec
mpv shipped as a bare package with no config, so every host decoded video
in software. hwdec is the only vendor-specific part, so it is an option:
auto-safe everywhere, overridden on NVIDIA hosts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:13:38 +01:00
Miguel Palhas 0d2a4cb86f chore(agent-skills): bump input to 0f131f0
CI / lint (push) Successful in 34s
CI / eval (push) Successful in 1m58s
Review sessions post findings on the PR instead of rev, entry paths
follow the checkout out of ~/tea/yolo, and the session's forge token is
named so reviewers stop reaching for the daemon's.
2026-08-22 19:16:30 +01:00
Miguel Palhas fc88ed82fd chore(agent-skills): bump input, drop two dokploy deny rules
CI / lint (push) Successful in 32s
CI / eval (push) Successful in 2m22s
Picks up sandboxed PR review sessions. The two dokploy entries were
already gone from the live settings file, so the mutable-file check
aborted every switch until nix matched it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:03:49 +01:00
Miguel Palhas fb01826479 chore(maestro): track the maestro flake from main
CI / lint (push) Successful in 35s
CI / eval (push) Successful in 2m14s
The nix branch landed, so the ?ref pin is no longer needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:03:07 +01:00
Miguel Palhas d932409986 feat(maestro): run the daemon and web UI on yolo
CI / eval (push) Successful in 2m39s
CI / lint (push) Successful in 42s
Adds the maestro flake as an input and imports its Home Manager module.
yolo enables both user services; web binds 0.0.0.0:8081 because aoe-web
already holds 8080 on this host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:55:47 +01:00
Miguel Palhas 7c7aa0a7d9 fix(aoe-web): drop removed --auth flag
CI / lint (push) Successful in 36s
CI / eval (push) Successful in 2m12s
2026-08-22 09:47:26 +01:00
Miguel Palhas f99e0d4a8f chore(claude): default model opus[1m] 2026-08-22 09:44:56 +01:00
Miguel Palhas d41572a51c chore(flake): bump agent-of-empires (npmDepsHash fix) 2026-08-22 09:39:17 +01:00
Miguel Palhas 0f059c02c8 chore(flake): bump agent-of-empires 2026-08-22 09:19:15 +01:00
Miguel Palhas 11f6ad5a3d feat(yolo): install claude-agent-acp for aoe structured sessions
CI / lint (push) Successful in 32s
CI / eval (push) Successful in 1m52s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 20:47:20 +01:00
Miguel Palhas 1ce7046994 fix(aoe-web): pin TMUX_TMPDIR and KillMode=process
CI / lint (push) Successful in 42s
CI / eval (push) Successful in 2m38s
Shells set TMUX_TMPDIR to the runtime dir; the unit did not, so the
daemon ran a second tmux server under /tmp. Every boot resumed duplicate
agents there, the dashboard diverged from the TUI, and polling the
invisible sessions fired an error push per session. Same fix pr-daemon
got.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 20:32:36 +01:00
Miguel Palhas 42fa03ec0e feat: switch aoe to git.naps.pt/yolo fork
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 2m8s
Fork carries two patches on v1.14.1+: worktree_info on plain add
(grouping) and reaping agent processes that survive tmux teardown.
Also brings upstream the settings drift the mutable-file guard caught:
fable model, secret-guard hook chain, verbose, confirm_delete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 19:55:22 +01:00
Miguel Palhas 1b52576097 fix: rev-deploy must skip unit install — units are hm symlinks
CI / lint (push) Successful in 36s
CI / eval (push) Successful in 1m52s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:04:48 +01:00
Miguel Palhas 522eda7644 fix(smb): restrict hosts allow to wg subnet 10.10/16
CI / lint (push) Successful in 35s
CI / eval (push) Successful in 2m4s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:21:39 +01:00
Miguel Palhas 25ca90f134 docs(smb): wg subnet is 10.10, not 10.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:21:03 +01:00
Miguel Palhas ccf0b2c35d fix(smb): widen hosts allow to 10/8 for wg clients
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:20:19 +01:00
Miguel Palhas 2532100b26 feat(smb): export yolo home, mount at ~/yolo
Guest samba share of /home/naps62 on yolo, limited to the private
nets via hosts allow. arrakis and konishi mount it with the same
lazy automount used for the NAS media share.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:17:34 +01:00
Miguel Palhas 9b4a2b44dc feat(opencode): allow bash on yolo only
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m1s
Global config keeps bash at "ask", which stalls every unattended opencode
session on git and grep. yolo runs the PR daemon's reviewers, so the prompt
is a deadlock there rather than a gate.

Merges like claude-settings.json: home/yolo/opencode.json holds only the
override, so agents, commands and skills still come from common. Reviewers
run on branches under review, so this drops that prompt too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:25:08 +01:00
Miguel Palhas 503b3baeb4 chore: bump claude-code to 2.1.237
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 2m1s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:30:24 +01:00
Miguel Palhas 7bdff2ff98 chore: bump remaining flake inputs
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m50s
2026-08-19 23:06:55 +01:00
Miguel Palhas 9794335ad6 chore: bump nixpkgs, home-manager, hardware
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m5s
2026-08-19 22:51:40 +01:00
Miguel Palhas 4a1e9aeaa8 fix(tmux): use csi-u extended keys format
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m45s
2026-08-19 22:45:21 +01:00
Miguel Palhas cd3b271e10 chore: remove ferdium
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m46s
2026-08-19 22:42:30 +01:00
Miguel Palhas 3eb0a098a2 chore: ignore worktree dirs
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m54s
2026-08-19 22:40:04 +01:00
Miguel Palhas 77d8803a8f refactor: scope workstation apps 2026-08-19 22:38:19 +01:00
Miguel Palhas ba5b618e49 refactor(yolo): take the agent-skills units from their own module
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m57s
pr-daemon, hourlog and week-review are defined by the agent-skills
home-manager module now, next to the scripts they run; this host just
enables them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:20:44 +01:00
Miguel Palhas 7580edb82c fix(pr-daemon): pin TMUX_TMPDIR to the runtime dir
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m53s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:06:33 +01:00
Miguel Palhas 88abb49340 fix(pr-daemon): KillMode=process
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m53s
Agent tmux sessions the daemon starts live in its cgroup; a restart was
killing every session it had created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:03:51 +01:00
Miguel Palhas 9c536f87f6 Merge branch 'pr-daemon'
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m52s
2026-08-19 18:54:33 +01:00
Miguel Palhas 88915d9cfd feat(hyprland): restore media-key OSD with swayosd
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m53s
The XF86 binds survived the noctalia -> eww switch, but the popup they
drew did not, so volume and brightness changed with no feedback. swayosd
replaces it: swayosd-client both applies the change and draws the OSD.

Also fixes two binds that pointed at binaries no host installs any more:
XF86AudioMicMute called volumectl (avizo, removed) and the brightness
keys called brightnessctl, which only arrakis has.

Commands are referenced by store path because Hyprland execs binds with
the PATH the session was started with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 18:35:27 +01:00
Miguel Palhas 94e24f9277 feat(yolo): run the PR daemon as a user service
Watches GitHub and Gitea PRs and routes them to aoe sessions. Lives here
rather than in agent-skills' nix module for the same reason the timers
do: it belongs to one box.

Config and tokens are outside the store, so the unit refuses to start
without them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 18:30:56 +01:00
Miguel Palhas 51c852b5ef chore: bump claude-code to 2.1.235
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m51s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:46:09 +01:00
Miguel Palhas 54fc3e317b refactor(sddm): replace noctalia theme with sddm-astronaut
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m53s
Noctalia shell is no longer used; the only consumer left was the
SDDM login theme.
2026-08-19 16:11:11 +01:00
Miguel Palhas 81b9cbde44 Merge branch 'update'
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m50s
2026-08-19 15:16:03 +01:00
Miguel Palhas 60f93e604f Merge branch 'tatars' 2026-08-19 15:15:18 +01:00
Miguel Palhas fc7ac103f5 chore: update agent-skills
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:15:16 +01:00
Miguel Palhas ced70ee3e3 chore: bump the agent-skills pin for pi + opencode coverage 2026-08-19 15:14:43 +01:00
Miguel Palhas 5ba12d3684 refactor: move pi's AGENTS.md concat into the agent-skills module
All four tools' context wiring lives there now; this file just installs
the package.
2026-08-19 15:14:43 +01:00
Miguel Palhas 231b6bdb28 refactor: get opencode rules and shared commands from agent-skills
opencode has no @file imports, so the shared home-manager module
concatenates the claude-md fragments into .config/opencode/AGENTS.md
(the screenshots line moved there as a fragment) and links commands/
into place. Keeps only the opencode-specific agents, commands and
skills here.
2026-08-19 15:14:43 +01:00
naps62-yolo a0a69cf58c feat: replace noctalia with an eww panel (#3)
CI / eval (push) Successful in 2m3s
CI / lint (push) Successful in 29s
2026-08-19 15:13:05 +01:00
Miguel Palhas d61700d173 fix: make synclaude reach synthetic.new
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m0s
Two dokploy MCP tools declare ttl with the draft-04 "exclusiveMinimum":
true. Synthetic validates tool schemas against draft-2020 and rejects the
whole request, so every session died on a bodiless 500. Deny those two.

Haiku moves to GLM Flash: Qwen3.6-27B rejects Claude Code multi-block
system prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:22:12 +01:00
Miguel Palhas b07120973a feat: add synclaude, Claude Code on synthetic.new
Binds the opus/sonnet/haiku aliases to Kimi K3, GLM-5.2 and Qwen3.6 so
they can be swapped mid-session, and registers the wrapper as an aoe agent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:01:06 +01:00
Miguel Palhas 074cb5faa1 chore: drop kimiclaude from aoe agents
Moonshot-backed wrapper is no longer used; only synclaude remains registered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:17:15 +01:00
naps62 46bb060208 Merge pull request 'chore: pin the noctalia input to a rev' (#2) from noctalia-pin into main
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 2m18s
Reviewed-on: #2
2026-08-19 13:11:36 +01:00
Miguel Palhas 189196fb88 chore: pin the noctalia input to a rev
CI / lint (pull_request) Successful in 29s
CI / eval (pull_request) Successful in 1m51s
noctalia publishes no binary cache, so `flake update` means a local Qt/QML
build every time. quickshell comes from nixpkgs and is cached, so the
nixpkgs follows stays; only the noctalia ref is pinned.

Pinned to the rev already locked, so this rebuilds nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:29:22 +01:00
Miguel Palhas 849cd3a5ef chore: sync aoe structured-session toggle from yolo
CI / lint (push) Successful in 38s
CI / eval (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:22:23 +01:00
Miguel Palhas 013d41aa72 feat: add the pi coding agent harness
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 2m9s
Packages the prebuilt pi release binary and puts it on PATH alongside
Claude Code, Codex and opencode.

pi has no `@file` imports in context files, so the shared agent-skills
fragments are concatenated into ~/.pi/agent/AGENTS.md. Skills need no
wiring: pi already reads ~/.agents/skills.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:57:24 +01:00
Miguel Palhas 0a4dd8a192 fix: bypass the ssh agent for github on yolo
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 2m4s
gpg-agent is the ssh agent here, and signing blocks forever on a pinentry
prompt with no TTY, so git push hung right after "Server accepts key".
The key file has no passphrase, so read it directly instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:11:28 +01:00
Miguel Palhas 8efd0a1139 feat: give yolo a distinct amber cursor
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m56s
yolo's desktop is only ever seen through a Moonlight window, so its cursor
needs to be tellable apart from the client's at a glance. Bibata-Modern-Amber
differs in both colour and silhouette from Nordzy.

Threads the theme through an option rather than setting it in one place: it
was hardcoded in cursor.nix, in hl.env, in the setcursor autostart, and twice
more in darkman's mode scripts, which re-set it on every light/dark switch and
would have reverted the override.

Also drops a stale hl.env pointing HYPRCURSOR_THEME at rose-pine-hyprcursor,
which no host installs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:06:01 +01:00
Miguel Palhas de527df7c4 feat: stream yolo at 4k
virtio-gpu exposes no NVENC/VAAPI, so sunshine software-encodes. Measured
x264 (superfast+zerolatency) on the 8 vCPUs: 73fps at 2160p against 135fps
at 1440p, so a 60fps client keeps roughly 20% margin.

Also brings the drifted agent-of-empires config upstream, which the
mutableFiles guard required before switching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:56:56 +01:00
Miguel Palhas c7eb5796ea fix: grant sunshine access to /dev/uinput
hardware.uinput, enabled by services.sunshine, sets /dev/uinput to
root:uinput 0660 and adds no members to the group. Sunshine runs as a
user service, so it failed to create the virtual mouse, keyboard and
gamepads and the stream carried no input.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:43:55 +01:00
61 changed files with 1802 additions and 1316 deletions
Generated
+243 -144
View File
@@ -7,27 +7,26 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1786554876,
"narHash": "sha256-akMknOIdX0QsBsM4lgreXSNNym7TNu9iOrDBqrQuTuQ=",
"owner": "agent-of-empires",
"repo": "agent-of-empires",
"rev": "9b0d691a6cdaa6d2451f6eda05e83628c54eb95a",
"type": "github"
"lastModified": 1787387938,
"narHash": "sha256-113jGEYKSeTwYgLdlVuRyqPBHYHJcU1kbRKiAUhj04Q=",
"ref": "refs/heads/main",
"rev": "96472052106cb5c448e9fa39244b0d0ccae9be72",
"revCount": 2192,
"type": "git",
"url": "https://git.naps.pt/yolo/agent-of-empires.git"
},
"original": {
"owner": "agent-of-empires",
"ref": "v1.14.1",
"repo": "agent-of-empires",
"type": "github"
"type": "git",
"url": "https://git.naps.pt/yolo/agent-of-empires.git"
}
},
"agent-skills": {
"locked": {
"lastModified": 1786957004,
"narHash": "sha256-lUyQGAN6Kd5onhPa+ptOiwtIaY6Y02q+nyRt9NWkdDg=",
"lastModified": 1788356316,
"narHash": "sha256-y5dQioiefegmuPLulFSEL4DrRJMaEHt1UcMGyOM6fRk=",
"ref": "refs/heads/main",
"rev": "7cb03f879600e87281356fdc411b2690f09a9175",
"revCount": 47,
"rev": "abd21928bf993962f98364d48a2877155beaa6d0",
"revCount": 111,
"type": "git",
"url": "https://git.naps.pt/yolo/agent-skills.git"
},
@@ -56,11 +55,11 @@
]
},
"locked": {
"lastModified": 1784368054,
"narHash": "sha256-zF1iJkBQSDWmRO4/LEeHR1SpKY0lqZaxkoQJpPS9K9U=",
"lastModified": 1786464219,
"narHash": "sha256-WKqWL8r7CyTDYueTr2ffJ9ya50dellv6IR1JX5PghDY=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "9b5f14d9483445e766294eb8fbe0b8f370269ed0",
"rev": "f3d1804205e8158c15595cdda1b566f93349ffae",
"type": "github"
},
"original": {
@@ -71,15 +70,15 @@
},
"claude-code": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs_2",
"systems": "systems"
},
"locked": {
"lastModified": 1785285785,
"narHash": "sha256-r1cMlvXjRVKe+uQ/GKwy4TpionpmbNgksFP2758D/MM=",
"lastModified": 1788303707,
"narHash": "sha256-ICEHLJAM480kNrsOQ+DPWWV3XFJUDc6/n/PPcjAuRR8=",
"owner": "sadjow",
"repo": "claude-code-nix",
"rev": "da78262708d858861afbe1f68ea65fedda4054c4",
"rev": "7f0aeb2f0c6cf1ca2bf3b0b76b823b24b04b5b21",
"type": "github"
},
"original": {
@@ -90,17 +89,17 @@
},
"codex-cli": {
"inputs": {
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785294467,
"narHash": "sha256-mylUWHK2wIw8S07K5hbkrLELnflJkDwsSWz2YFbJzLI=",
"lastModified": 1787094470,
"narHash": "sha256-UiHfeA8umiWByi/ijDnfnlGcVSEAhwNv1Gue3h6ey/U=",
"owner": "sadjow",
"repo": "codex-cli-nix",
"rev": "e4e3b0672bbb8fba7f32fe53cd9c604990970374",
"rev": "cfbeeca2f4dfbbfcc3c1a5b21e61a051ff43533d",
"type": "github"
},
"original": {
@@ -112,17 +111,17 @@
},
"codex-desktop-linux": {
"inputs": {
"flake-utils": "flake-utils_3",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1786822283,
"narHash": "sha256-RdoFKwNQ2m2zbHnq/QbgLf2lUI9cohsnlMwLfJuaTh0=",
"lastModified": 1787079200,
"narHash": "sha256-BYo8mVEqRj6y8PzpFd3rX6TQTknB3Ynp/3ykMTm56D8=",
"owner": "ilysenko",
"repo": "codex-desktop-linux",
"rev": "e6b51d96ac2b9b5d9adafa852241bd0982972557",
"rev": "1875dc2eaab6f9448617d18d0eda58902edf0f1a",
"type": "github"
},
"original": {
@@ -146,6 +145,21 @@
"type": "github"
}
},
"crane_2": {
"locked": {
"lastModified": 1787326676,
"narHash": "sha256-lWhBbBvC05/xwivKBBiM2YNizpmgqCgyOIzomvRuwxs=",
"owner": "ipetkov",
"repo": "crane",
"rev": "692f7e9ef2ece8125b466f66f2af532b3edaed0d",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"ethui": {
"inputs": {
"nixpkgs": "nixpkgs_3",
@@ -205,6 +219,24 @@
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_3"
},
"locked": {
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
@@ -220,24 +252,6 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
@@ -255,7 +269,7 @@
"type": "github"
}
},
"flake-utils_3": {
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
@@ -273,7 +287,7 @@
"type": "github"
}
},
"flake-utils_4": {
"flake-utils_3": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
@@ -288,7 +302,7 @@
"type": "github"
}
},
"flake-utils_5": {
"flake-utils_4": {
"inputs": {
"systems": "systems_8"
},
@@ -308,17 +322,17 @@
},
"foundry": {
"inputs": {
"flake-utils": "flake-utils_4",
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785311662,
"narHash": "sha256-lC5ZlyNMf8wcQnrAzUhYb1p6ndzwEovnXKiMqSTLgUo=",
"lastModified": 1787123762,
"narHash": "sha256-gKUrkWZRS2BxEcJDyVSBZJ3fi1rN51PPe8U5hCjFNIY=",
"owner": "shazow",
"repo": "foundry.nix",
"rev": "0eb3d1233237eb4e6f4f45f31c087ee1a16245f1",
"rev": "a194d90ec9f6a4d70d79220e644c531da8324266",
"type": "github"
},
"original": {
@@ -334,11 +348,11 @@
]
},
"locked": {
"lastModified": 1785232496,
"narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=",
"lastModified": 1787144466,
"narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98",
"rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"type": "github"
},
"original": {
@@ -355,11 +369,11 @@
]
},
"locked": {
"lastModified": 1785306346,
"narHash": "sha256-DScBkW0fOgpGPK2trNoX3ryLTlaC14+gglFo/BhGJ4g=",
"lastModified": 1787176219,
"narHash": "sha256-djoRr6jBpe35q/0JwAvXpFXg1Ktf+X54NU2RLR7wnHw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "e705714e918c3b11affcdd15db2cbe3a070420a0",
"rev": "c53d643b3737e2fcd04e6cb3b3580ef50b2087a0",
"type": "github"
},
"original": {
@@ -376,11 +390,11 @@
]
},
"locked": {
"lastModified": 1783963347,
"narHash": "sha256-r376E2XpakiXwModDHIxlvB6qLq4iFVEq730vxOO4JY=",
"lastModified": 1786719456,
"narHash": "sha256-B74DLQs/VjlqyhnnX3tguWwghqJHWSJX30TKZuAljIg=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "a45a7c451455a51ae740ec3bce4024b312809c29",
"rev": "83b7606dcf44abe3a94b86e8bb2b3355d22e8797",
"type": "github"
},
"original": {
@@ -405,11 +419,11 @@
]
},
"locked": {
"lastModified": 1776511930,
"narHash": "sha256-fCpwFiTW0rT7oKJqr3cqHMnkwypSwQKpbtUEtxdkgrM=",
"lastModified": 1786464181,
"narHash": "sha256-2alOMkLjXANh7unkZnYnCF2K2rApZaOLMoQ3o+VX2CY=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "39435900785d0c560c6ae8777d29f28617d031ef",
"rev": "e4ed7c08123df5af460a0a70961380cbfb872f76",
"type": "github"
},
"original": {
@@ -434,11 +448,11 @@
]
},
"locked": {
"lastModified": 1782566056,
"narHash": "sha256-haEZcHzYrePnjFOYSWTbxm/Nrla0aPslJfmvdCvqtVc=",
"lastModified": 1786464367,
"narHash": "sha256-k58p4wbzIXWyRWrW84pP8tD+iaZSSYiiM+fr0Auk4oU=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "c6e7b9f673f4360bc813d3dc75028f75ee88d3f8",
"rev": "7c895c44e3ca6d28ed68ddd80ec02b02b925e7fc",
"type": "github"
},
"original": {
@@ -464,11 +478,11 @@
"xdph": "xdph"
},
"locked": {
"lastModified": 1785334807,
"narHash": "sha256-bN0KRhDfwSPqbpgUgWVcQMkuDje+8ghWlm7uUQZzaxc=",
"lastModified": 1787138659,
"narHash": "sha256-/3fIl+/xZoZqliAfwYB8ahCZK04Qr/zwdOQgiR41dy8=",
"ref": "refs/heads/main",
"rev": "7803e68638c1bebce57cf9ea8da51fd9a1e19edd",
"revCount": 7669,
"rev": "52b368f1d7fdee1b0e96ef3dc655b5a577df4fdb",
"revCount": 7745,
"submodules": true,
"type": "git",
"url": "https://github.com/hyprwm/Hyprland"
@@ -512,11 +526,11 @@
]
},
"locked": {
"lastModified": 1784196523,
"narHash": "sha256-ahtKMGXFJdlQNhatQm1+BBU/pGfGYnAqQt3vWvq4p8s=",
"lastModified": 1786464504,
"narHash": "sha256-7sHwM86KILQyHDHDuE2SDBlQ2jvZ0EW3hY7sW009/cg=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "a6ccb6cb112ed5a244c0191fb972347ecfa893e0",
"rev": "4c30cf3097ea963c0e250749ee0c59f8b08816d6",
"type": "github"
},
"original": {
@@ -566,11 +580,11 @@
]
},
"locked": {
"lastModified": 1777320127,
"narHash": "sha256-Qu+Wf2Bp5qUjyn2YpZNq8a7JyzTGowhT1knrwE38a9U=",
"lastModified": 1786464129,
"narHash": "sha256-339AkTlpMYSIvFuG0rnR+8Yg4/AZKeJalshJavlnKfg=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "090117506ddc3d7f26e650ff344d378c2ec329cc",
"rev": "9508458be316a0d70d37ebed1ab725ccd10411ff",
"type": "github"
},
"original": {
@@ -640,11 +654,11 @@
]
},
"locked": {
"lastModified": 1782554491,
"narHash": "sha256-+p3MlyN/nqRefcf2IckPlGRUn9+hielqpS9XClbLleM=",
"lastModified": 1785930473,
"narHash": "sha256-DitTu625BhEYpZjtjxtGpjrEJwPwW+X/+jJvhSZNSJM=",
"owner": "hyprwm",
"repo": "hyprtoolkit",
"rev": "bdba25ced39ea39ab004a8f31593ba0b0ff1ca35",
"rev": "af515b69dfbe366dc7873aa1475cb2f4db3ebad7",
"type": "github"
},
"original": {
@@ -665,11 +679,11 @@
]
},
"locked": {
"lastModified": 1784323413,
"narHash": "sha256-XnAVV+H4f8Xdv0yZcSwJ5kCjLyE8fHxPeLX6a3HSrAU=",
"lastModified": 1786464080,
"narHash": "sha256-W1hxvumEM57yV+QwsZ4QdAHEqOkr7e4S8VAkLX60qDE=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "5f03477ab3a005ff27c527486f551883535aea2f",
"rev": "c157fe1e3092b980cc69315a6631f89aff09dcce",
"type": "github"
},
"original": {
@@ -690,11 +704,11 @@
]
},
"locked": {
"lastModified": 1777159683,
"narHash": "sha256-Jxixw6wZphUp+nHYxOKUYSckL17QMBx2d5Zp0rJHr1g=",
"lastModified": 1786464033,
"narHash": "sha256-QM8Qe4/L8lpdVN4bgwahmi+jyyc4fisseDMe4afcDxA=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "b8632713a6beaf28b56f2a7b0ab2fb7088dbb404",
"rev": "62e62c1ca23da17612c6890d4ad2064f575643db",
"type": "github"
},
"original": {
@@ -719,11 +733,11 @@
]
},
"locked": {
"lastModified": 1778410714,
"narHash": "sha256-o6RzFj4nJXaPRY7EM01siuCQeT41RfwwmcmFQqwFJJg=",
"lastModified": 1786464294,
"narHash": "sha256-ZQsZ2WvBdkboCIyh8LStDPdAIARmxzn0XMNxxoOhjPE=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "85148a8e612808cf5ddb25d0b3c5840f3498a7dc",
"rev": "4ce7cd6b6128c1ac41caf23c58a30a26b327f9dd",
"type": "github"
},
"original": {
@@ -732,6 +746,43 @@
"type": "github"
}
},
"maestro": {
"inputs": {
"crane": "crane_2",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_6",
"process-compose-flake": "process-compose-flake"
},
"locked": {
"lastModified": 1788472721,
"narHash": "sha256-meDGgW8/Wf8vh1Yt/PqsAq3tkQ1s5fKn/29tvBkc2UU=",
"ref": "refs/heads/main",
"rev": "8325bc6d9a9a6c74995737af121114c365b17b44",
"revCount": 617,
"type": "git",
"url": "https://git.naps.pt/naps62/maestro.git"
},
"original": {
"type": "git",
"url": "https://git.naps.pt/naps62/maestro.git"
}
},
"nix-flatpak": {
"locked": {
"lastModified": 1767983141,
"narHash": "sha256-7ZCulYUD9RmJIDULTRkGLSW1faMpDlPKcbWJLYHoXcs=",
"owner": "gmodena",
"repo": "nix-flatpak",
"rev": "440818969ac2cbd77bfe025e884d0aa528991374",
"type": "github"
},
"original": {
"owner": "gmodena",
"ref": "latest",
"repo": "nix-flatpak",
"type": "github"
}
},
"nix-index-database": {
"inputs": {
"nixpkgs": [
@@ -739,11 +790,11 @@
]
},
"locked": {
"lastModified": 1785046085,
"narHash": "sha256-UiK+mmZJuLWQVhJ5b2wDzogIYWAesyRm6LA3h3Ulh3Y=",
"lastModified": 1786852476,
"narHash": "sha256-IM5CYtf86W4w8eUPpKcY/LpdHElmVBtJhaKnoTKxZEA=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "11665045df8b9938ef811a3bfdc65cffb02b4b70",
"rev": "c7962dc97b45129df8d751bedaf37beb5a17706e",
"type": "github"
},
"original": {
@@ -784,6 +835,21 @@
}
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1785031560,
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-lib_3": {
"locked": {
"lastModified": 1774748309,
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
@@ -800,11 +866,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1784783405,
"narHash": "sha256-4IHyyLgLBdKefkljdKod4IMn023pQiDXAWJA187cmdY=",
"lastModified": 1787964612,
"narHash": "sha256-0N9nghg3nwzX6b6qc77EzjR9cu/Z+UR66FlfsCqiURs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7525d999cd850b9a488817abc89c75dc733acf17",
"rev": "e8be7818e19ada32105a8af937a6a473b38167ca",
"type": "github"
},
"original": {
@@ -848,11 +914,11 @@
},
"nixpkgs_5": {
"locked": {
"lastModified": 1784356753,
"narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=",
"lastModified": 1786247143,
"narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "61b7c44c4073f0b827768aff0049561b5110ea5a",
"rev": "279b4a8275f032c566576b3f181fa0f27197f588",
"type": "github"
},
"original": {
@@ -864,11 +930,27 @@
},
"nixpkgs_6": {
"locked": {
"lastModified": 1785090369,
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
"lastModified": 1787135253,
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1787070829,
"narHash": "sha256-vXNVDVtvfiQuXthP0NHPFdNvvMTkGpx0UP8oddIWbNk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "624af665418d3c65d544145b4d34ad696439570e",
"rev": "0ae2bc1419c3f345984c2629e72e7a631820fa4d",
"type": "github"
},
"original": {
@@ -878,7 +960,7 @@
"type": "github"
}
},
"nixpkgs_7": {
"nixpkgs_8": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
@@ -894,7 +976,7 @@
"type": "github"
}
},
"nixpkgs_8": {
"nixpkgs_9": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
@@ -910,26 +992,6 @@
"type": "github"
}
},
"noctalia": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785335804,
"narHash": "sha256-oN3I6Kr71lOCE3/ou4GgY2cQYh4B6xQxZXzomFgYn0s=",
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "aded31799eb0dffdbcf436243083f1ebce26f95d",
"type": "github"
},
"original": {
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"type": "github"
}
},
"nvchad-starter": {
"flake": false,
"locked": {
@@ -956,11 +1018,11 @@
]
},
"locked": {
"lastModified": 1785121894,
"narHash": "sha256-AWbstkwe4twd+5iWx8QBVzTgwGAUvn4vVdobGKW5BRc=",
"lastModified": 1786797655,
"narHash": "sha256-qAWI5Vf0xaRmb8/l9XWYrvEgrIaqKBv8Dg81JO7wkrY=",
"owner": "nix-community",
"repo": "nix4nvchad",
"rev": "8135d402aa554b5f8e5dd2ec341c5a440fd18496",
"rev": "b878640ae13c8f70f41aa1159234206051a4378c",
"type": "github"
},
"original": {
@@ -991,6 +1053,41 @@
"type": "github"
}
},
"process-compose-flake": {
"locked": {
"lastModified": 1782060835,
"narHash": "sha256-Q8HH2t1l3MITtWCWY4ytcH5F/Q7aAHo3Iq/QTMAKTe0=",
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"rev": "464ff6880737f063c3f0d3d2c7781fda9190868f",
"type": "github"
},
"original": {
"owner": "Platonic-Systems",
"repo": "process-compose-flake",
"type": "github"
}
},
"rev": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1788356169,
"narHash": "sha256-NpIwwZ3O+rahCrWkYJvZEcSM0FeoO0av7bR7MDgZNZ0=",
"ref": "refs/heads/main",
"rev": "b432e8a9ed4e8c819df1aaf547161b71967395d3",
"revCount": 235,
"type": "git",
"url": "https://git.naps.pt/yolo/rev.git"
},
"original": {
"type": "git",
"url": "https://git.naps.pt/yolo/rev.git"
}
},
"root": {
"inputs": {
"agent-of-empires": "agent-of-empires",
@@ -1003,11 +1100,13 @@
"hardware": "hardware",
"home-manager": "home-manager",
"hyprland": "hyprland",
"maestro": "maestro",
"nix-flatpak": "nix-flatpak",
"nix-index-database": "nix-index-database",
"nixpkgs": "nixpkgs_6",
"noctalia": "noctalia",
"nixpkgs": "nixpkgs_7",
"nvchad-starter": "nvchad-starter",
"nvchad4nix": "nvchad4nix",
"rev": "rev",
"rose-pine-hyprcursor": "rose-pine-hyprcursor",
"sem": "sem",
"spicetify-nix": "spicetify-nix",
@@ -1057,7 +1156,7 @@
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_8"
"nixpkgs": "nixpkgs_9"
},
"locked": {
"lastModified": 1775445266,
@@ -1075,9 +1174,9 @@
},
"sem": {
"inputs": {
"flake-parts": "flake-parts_2",
"flake-utils": "flake-utils_5",
"nixpkgs": "nixpkgs_7",
"flake-parts": "flake-parts_3",
"flake-utils": "flake-utils_4",
"nixpkgs": "nixpkgs_8",
"rust-overlay": "rust-overlay"
},
"locked": {
@@ -1102,11 +1201,11 @@
"systems": "systems_9"
},
"locked": {
"lastModified": 1785048222,
"narHash": "sha256-LjFVfwxensz76SjnvofF1Jsw+xTLd8Qp31W0J6nh2XI=",
"lastModified": 1786855359,
"narHash": "sha256-yeKMWFCPeoIKmPBLLvP1/15ulOJO8i9ZIlSSXGuW6aw=",
"owner": "Gerg-L",
"repo": "spicetify-nix",
"rev": "17e524f330c282d31c32dfe076222d4a12277886",
"rev": "0f478ff79b82abb785160cd4531293f61d21be86",
"type": "github"
},
"original": {
@@ -1329,11 +1428,11 @@
]
},
"locked": {
"lastModified": 1784371182,
"narHash": "sha256-S8A1lezEalltWcCp3gAic5lssS0xTSISK6fKODefhOk=",
"lastModified": 1786464334,
"narHash": "sha256-/TBQT5rhBB2Dm4HoZzhGDaCwYmRTs3W3DPhMXFWc/BU=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "08d99f727944dd15e4740090305e31c5fb92a50a",
"rev": "9f0e9ff02739cd538d39bd706422dc50e9ca60dd",
"type": "github"
},
"original": {
@@ -1350,11 +1449,11 @@
]
},
"locked": {
"lastModified": 1785224333,
"narHash": "sha256-8DNJUVM10ktK02V9ZIV064CH+fuPF272VCAZBn6arig=",
"lastModified": 1787160391,
"narHash": "sha256-5dRTU55IlWABqnWiXDgtzrc3wT54nPoZwDQ91VOY4mE=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "5f1a4be4d280e1ae06d61a75775a234b1714da3f",
"rev": "228a12a50971a115caa1c87b7de3d87b08e35275",
"type": "github"
},
"original": {
+14 -5
View File
@@ -49,10 +49,6 @@
url = "https://github.com/hyprwm/Hyprland";
submodules = true;
};
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
spicetify-nix = {
url = "github:Gerg-L/spicetify-nix";
inputs.nixpkgs.follows = "nixpkgs";
@@ -68,12 +64,25 @@
};
# Pinned to the tag, and pins its own nixpkgs for the same reason ethui does
# — it is a verified Rust build. Bump deliberately, not via `flake update`.
agent-of-empires.url = "github:agent-of-empires/agent-of-empires/v1.14.1";
agent-of-empires.url = "git+https://git.naps.pt/yolo/agent-of-empires.git";
# Semantic-diff tool rev calls via REV_SEM_BIN. NOT nixpkgs' `sem`, which is
# the unrelated Semaphore CI cli.
sem.url = "github:Ataraxy-Labs/sem";
# Claude Code + Codex skills, commands, hooks and CLAUDE.md fragments.
agent-skills.url = "git+https://git.naps.pt/yolo/agent-skills.git";
# Terminal-session orchestrator. Pins its own nixpkgs for the same reason
# ethui and agent-of-empires do — it is a verified Rust build.
maestro.url = "git+https://git.naps.pt/naps62/maestro.git";
# Always-on local code review server. Follows nixpkgs, unlike the Rust
# inputs above: it is a plain node bundle, and a second nixpkgs would put a
# second node 26 in the closure for nothing.
rev = {
url = "git+https://git.naps.pt/yolo/rev.git";
inputs.nixpkgs.follows = "nixpkgs";
};
# Declarative flatpak remotes and packages; nixpkgs' services.flatpak only
# exposes `enable`. Has no nixpkgs input to follow.
nix-flatpak.url = "github:gmodena/nix-flatpak/?ref=latest";
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
+4
View File
@@ -5,6 +5,10 @@
imports = [
../common/programs/default.nix
../common/programs/desktop
../common/programs/workstation-apps.nix
../common/programs/editors.nix
../common/programs/dev-workstation.nix
../common/programs/ai-gui.nix
../common/programs/zen-browser.nix
../common/programs/hyprland
../common/programs/kitty
-105
View File
@@ -1,105 +0,0 @@
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.home.mutableFiles;
repoPath = config.home.mutableFilesRepoPath;
flakePrefix = self.outPath;
fileEntries = lib.attrsToList cfg;
toRepoPath = storePath: repoPath + lib.removePrefix flakePrefix (toString storePath);
checkScript = lib.concatMapStringsSep "\n" (
{ name, value }:
let
target = "${config.home.homeDirectory}/${name}";
storePath = value.source;
# toRepoPath only works for sources that live in the flake tree. A
# generated source is not under flakePrefix, so removePrefix is a no-op
# and the hint would print a /nix/store path to copy back onto.
originalPath =
if value.upstreamPath != null then "${repoPath}/${value.upstreamPath}" else toRepoPath value.source;
in
''
if [ -f "${target}" ] && ! ${lib.getExe' pkgs.diffutils "diff"} -q "${storePath}" "${target}" > /dev/null 2>&1; then
echo ""
echo "!! mutable file changed: ${name}"
echo " To bring changes upstream:"
echo " cp ${target} ${originalPath}"
echo ""
${lib.getExe' pkgs.diffutils "diff"} -u "${storePath}" "${target}" || true
_mutable_changed=1
fi
''
) fileEntries;
copyScript = lib.concatMapStringsSep "\n" (
{ name, value }:
let
target = "${config.home.homeDirectory}/${name}";
inherit (value) source;
dirName = builtins.dirOf target;
in
''
mkdir -p "${dirName}"
cp -f "${source}" "${target}"
chmod ${if value.executable then "755" else "644"} "${target}"
''
) fileEntries;
in
{
options.home.mutableFilesRepoPath = lib.mkOption {
type = lib.types.str;
description = "Absolute path to the nixos-config repo on disk.";
};
options.home.mutableFiles = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
source = lib.mkOption {
type = lib.types.path;
description = "Path to the source file.";
};
executable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the file should be executable.";
};
upstreamPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "home/yolo/claude-settings.json";
description = ''
Repo-relative file to name in the "bring changes upstream" hint.
Required when `source` is generated rather than a file in the
flake tree, since the store path cannot be mapped back.
'';
};
};
}
);
default = { };
description = "Files to copy (not symlink) into the home directory, with change detection.";
};
config = lib.mkIf (cfg != { }) {
home.activation.mutableFiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
_mutable_changed=0
${checkScript}
if [ "$_mutable_changed" -eq 1 ]; then
echo ""
echo "!! Aborting: mutable files have been modified outside of nix."
echo " Bring the changes upstream first, then re-run."
exit 1
fi
${copyScript}
'';
};
}
+34 -11
View File
@@ -1,20 +1,43 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.custom.blender;
in
{
home.packages = with pkgs; [
blender
prusa-slicer
];
options.custom.blender = {
cuda = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Build Blender with CUDA/OptiX so Cycles renders on the GPU instead of
the CPU. Only useful on an NVIDIA host; enabling it on arrakis (Intel)
would mean a long build for nothing.
fonts.fontconfig.enable = true;
No binary cache serves this cuda-maintainers was measured to make no
difference so flipping this on rebuilds Blender, OpenUSD, OpenSubdiv
and OpenImageDenoise from source.
'';
};
};
xdg.desktopEntries.prusaslicer-url-handler = {
name = "PrusaSlicer Protocol Handler";
exec = "prusa-slicer %u";
type = "Application";
noDisplay = true;
mimeType = [ "x-scheme-handler/prusaslicer" ];
config = {
home.packages = [
(if cfg.cuda then pkgs.blender.override { cudaSupport = true; } else pkgs.blender)
pkgs.prusa-slicer
];
fonts.fontconfig.enable = true;
xdg.desktopEntries.prusaslicer-url-handler = {
name = "PrusaSlicer Protocol Handler";
exec = "prusa-slicer %u";
type = "Application";
noDisplay = true;
mimeType = [ "x-scheme-handler/prusaslicer" ];
};
};
}
+29
View File
@@ -0,0 +1,29 @@
{
config,
pkgs,
inputs,
lib,
...
}:
let
claude-desktop = pkgs.callPackage ../../../pkgs/claude-desktop/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
codex-cli = inputs.codex-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;
t3-code = pkgs.callPackage ../../../pkgs/t3-code/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
imports = [ inputs.codex-desktop-linux.homeManagerModules.default ];
home.packages = lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [
claude-desktop
t3-code
];
programs.codexDesktopLinux = {
enable = true;
cliPackage = codex-cli;
};
}
+18 -28
View File
@@ -1,18 +1,14 @@
{
config,
pkgs,
inputs,
lib,
...
}:
let
# Official Anthropic Claude desktop app (Linux build, x86_64 only).
claude-desktop = pkgs.callPackage ../../../../pkgs/claude-desktop/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
imports = [ inputs.agent-skills.homeModules.default ];
imports = [
inputs.agent-skills.homeModules.default
./synthetic.nix
];
# Shared UI-scale knob for the Electron AI desktop apps (Claude Desktop, T3 Code).
# Set per-host (e.g. konishi's 4K@1x monitors want ~"1.5"); null = native scale.
@@ -23,24 +19,24 @@ in
description = "--force-device-scale-factor value for Claude Desktop and T3 Code.";
};
# ~/.claude/settings.json stays unmanaged: Claude Code rewrites it itself
# (model pins, permission grants, plugin state), so any nix copy drifts within
# a session and every `nh home switch` then aborts on the diff.
config.home = {
packages =
with pkgs;
[
inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default
packages = with pkgs; [
inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default
# sandbox
bubblewrap
socat
libseccomp
# sandbox
bubblewrap
socat
libseccomp
# voice
sox
# voice
sox
# beads
dolt
]
++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 claude-desktop;
# beads
dolt
];
file = {
".default-npm-packages".text = ''
@@ -48,17 +44,11 @@ in
@beads/bd
'';
".claude/statusline.sh" = {
source = ./statusline.sh;
executable = true;
};
};
mutableFiles.".claude/settings.json".source = ./settings.json;
};
}
-86
View File
@@ -1,86 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Edit",
"Write",
"Bash(ls:*)",
"Bash(tree:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(find:*)",
"Bash(fd:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(wc:*)",
"Bash(sort:*)",
"Bash(uniq:*)",
"Bash(diff:*)",
"Bash(pwd:*)",
"Bash(which:*)",
"Bash(jq:*)",
"Bash(git:*)",
"Bash(gh:*)",
"Bash(sed:*)",
"Bash(cp:*)",
"Bash(chmod:*)",
"Bash(mkdir:*)",
"Bash(kitty @ set-tab-title:*)"
],
"additionalDirectories": [
"/home/naps62/projects",
"/home/naps62/ethui",
"/home/naps62/labs",
"/home/naps62/subvisual"
]
},
"model": "opus[1m]",
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
},
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true
},
"extraKnownMarketplaces": {
"impeccable": {
"source": {
"source": "github",
"repo": "pbakaus/impeccable"
}
}
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowedNetworkHosts": [
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"gist.github.com",
"release-assets.githubusercontent.com"
]
},
"tui": "fullscreen",
"voiceEnabled": true,
"defaultMode": "acceptEdits",
"feedbackSurveyState": {
"lastShownTime": 1754052643456
},
"mcpServers": {
"herd-mcp": {
"type": "http",
"url": "https://mcp.herd.eco/v1"
},
"linear": {
"type": "http",
"url": "https://mcp.linear.app/mcp"
},
"home-assistant": {
"type": "http",
"url": "https://ha-mcp.n62.casa/mcp"
}
},
"agentPushNotifEnabled": true
}
+76
View File
@@ -0,0 +1,76 @@
{
pkgs,
inputs,
...
}:
# Claude Code pointed at synthetic.new. The three model aliases below are bound
# to three Synthetic models, so `/model opus|sonnet|haiku` swaps between them
# live; anything else there is reachable by full id (`/model hf:...`).
let
claude = inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default;
# `hf:` ids pin one exact model. The `syn:large:*` aliases in Synthetic's own
# docs move under you whenever they rotate what "large" means.
kimi = "hf:moonshotai/Kimi-K3";
glm = "hf:zai-org/GLM-5.2";
# Not Qwen3.6-27B for the haiku slot: its chat template rejects Claude Code's
# multi-block system prompt with "System message must be at the beginning."
glmFlash = "hf:zai-org/GLM-4.7-Flash";
in
{
home.packages = [
(pkgs.writeShellScriptBin "synclaude" ''
set -euo pipefail
if [ -z "''${SYNTHETIC_API_KEY:-}" ] && [ -r "$HOME/.env.claude" ]; then
set +u
# Silenced: the file greets missing tooling on stderr, which would
# otherwise print before every session.
. "$HOME/.env.claude" >/dev/null 2>&1
set -u
fi
if [ -z "''${SYNTHETIC_API_KEY:-}" ]; then
echo "synclaude: SYNTHETIC_API_KEY is not set" >&2
exit 1
fi
export ANTHROPIC_BASE_URL="https://api.synthetic.new/anthropic"
export ANTHROPIC_AUTH_TOKEN="$SYNTHETIC_API_KEY"
export ANTHROPIC_DEFAULT_OPUS_MODEL="${kimi}"
export ANTHROPIC_DEFAULT_SONNET_MODEL="${glm}"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="${glmFlash}"
export CLAUDE_CODE_SUBAGENT_MODEL="${glm}"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_ATTRIBUTION_HEADER=0
# ~/.env.claude exports both. ANTHROPIC_MODEL outranks the DEFAULT_* trio
# above, and ANTHROPIC_API_KEY would send Anthropic's credential to
# Synthetic.
unset ANTHROPIC_MODEL ANTHROPIC_API_KEY
# These two dokploy tools spell ttl's bound `"exclusiveMinimum": true`,
# which is draft-04. Synthetic validates against draft-2020, where the key
# must be a number, and rejects the whole request a bare 500 with no body
# when streaming. Every session fails, not just ones that call these.
broken_tools=(
mcp__dokploy__dnsProvider-createRecord
mcp__dokploy__dnsProvider-updateRecord
)
# ~/.claude/settings.json pins model "opus[1m]". The [1m] context-window
# suffix survives alias resolution and reaches Synthetic as part of the id,
# which 404s there so pick the alias explicitly unless the caller did.
for arg in "$@"; do
case "$arg" in
--model | --model=*)
exec ${claude}/bin/claude --disallowedTools "''${broken_tools[@]}" "$@"
;;
esac
done
exec ${claude}/bin/claude --model sonnet --disallowedTools "''${broken_tools[@]}" "$@"
'')
];
}
-9
View File
@@ -7,14 +7,5 @@ let
codex-cli = inputs.codex-cli.packages.${pkgs.stdenv.hostPlatform.system}.default;
in
{
imports = [ inputs.codex-desktop-linux.homeManagerModules.default ];
# cliPackage only tells the desktop app where to find codex; it does not put
# it on PATH, so the cli needs installing separately.
home.packages = [ codex-cli ];
programs.codexDesktopLinux = {
enable = true;
cliPackage = codex-cli;
};
}
-11
View File
@@ -1,16 +1,13 @@
{
lib,
pkgs,
config,
...
}:
{
imports = [
../features/mutable-file.nix
./zsh.nix
./nix.nix
./neovim
./editors.nix
./rust.nix
./elixir.nix
./nodejs.nix
@@ -30,14 +27,6 @@
};
home = {
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
# this with a plain assignment.
mutableFilesRepoPath = lib.mkDefault "${config.home.homeDirectory}/projects/nixos-config";
packages = with pkgs; [
impala
];
username = lib.mkDefault "naps62";
homeDirectory = lib.mkDefault "/home/${config.home.username}";
stateVersion = lib.mkDefault "24.05";
+12 -15
View File
@@ -1,4 +1,7 @@
{ pkgs, ... }:
{ pkgs, config, ... }:
let
inherit (config.custom.hyprland) cursorTheme cursorSize;
in
{
home.packages = with pkgs; [
dconf
@@ -15,17 +18,14 @@
'';
cursor-theme = ''
${pkgs.dconf}/bin/dconf write \
/org/gnome/desktop/interface/cursor-theme "'Nordzy-cursors'"
/org/gnome/desktop/interface/cursor-theme "'${cursorTheme.dark}'"
# Set environment variables for current session
export HYPRCURSOR_THEME="Nordzy-cursors"
export XCURSOR_THEME="Nordzy-cursors"
export HYPRCURSOR_THEME="${cursorTheme.dark}"
export XCURSOR_THEME="${cursorTheme.dark}"
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor Nordzy-cursors 24
'';
noctalia-theme = ''
noctalia msg theme-mode-set dark
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.dark} ${toString cursorSize}
'';
};
lightModeScripts = {
@@ -37,17 +37,14 @@
'';
cursor-theme = ''
${pkgs.dconf}/bin/dconf write \
/org/gnome/desktop/interface/cursor-theme "'Nordzy-white'"
/org/gnome/desktop/interface/cursor-theme "'${cursorTheme.light}'"
# Set environment variables for current session
export HYPRCURSOR_THEME="Nordzy-white"
export XCURSOR_THEME="Nordzy-white"
export HYPRCURSOR_THEME="${cursorTheme.light}"
export XCURSOR_THEME="${cursorTheme.light}"
# Update Hyprland cursor theme
${pkgs.hyprland}/bin/hyprctl setcursor Nordzy-white 24
'';
noctalia-theme = ''
noctalia msg theme-mode-set light
${pkgs.hyprland}/bin/hyprctl setcursor ${cursorTheme.light} ${toString cursorSize}
'';
};
};
+20 -42
View File
@@ -1,13 +1,13 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
./darkman.nix
./spicetify.nix
# mpv is installed by programs.mpv there, not as a bare package here.
../mpv.nix
];
home = {
@@ -15,43 +15,21 @@
# various
google-chrome
thunar
obsidian
mpv
# screen recording — evaluating these
obs-studio
kooha # simple Wayland-native screen+audio recorder
vokoscreen-ng # GUI recorder with webcam overlay
gimp
font-manager
imv
pavucontrol
zathura
libsForQt5.qt5ct
kdePackages.qt6ct
nwg-look
xournalpp
jq
ffmpeg
unzip
# remote desktop
remmina
# networking
networkmanagerapplet
mtr
dnsutils
# communication
slack
ferdium
signal-desktop
zoom-us
# dev tools
yaak
bun
# themes
tela-icon-theme
];
@@ -83,36 +61,36 @@
};
xdg.configFile = {
# zathura: include noctalia-generated theme
"zathura/zathurarc".text = ''
include noctaliarc
set default-bg "#2e3440"
set default-fg "#eceff4"
set statusbar-bg "#3b4252"
set statusbar-fg "#eceff4"
set inputbar-bg "#3b4252"
set inputbar-fg "#eceff4"
set highlight-color "#ebcb8b"
set highlight-active-color "#88c0d0"
set recolor-lightcolor "#2e3440"
set recolor-darkcolor "#eceff4"
'';
# gtk: include noctalia-generated css (mkForce to override gtk module)
"gtk-3.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
"gtk-4.0/gtk.css".text = lib.mkForce ''
@import url("noctalia.css");
'';
# qt: use noctalia color scheme. Applied via QT_QPA_PLATFORMTHEME=qt6ct,
# set in the Hyprland env block (and imported into the dbus activation
# environment so the xdph screen-share picker inherits it too).
# custom_palette=true is required for qt5ct/qt6ct to actually apply the
# color scheme; Fusion is used because it fully honors a custom palette
# (Breeze/native styles partly ignore it).
# qt: dark color scheme shipped by qt5ct/qt6ct themselves. Applied via
# QT_QPA_PLATFORMTHEME=qt6ct, set in the Hyprland env block (and imported
# into the dbus activation environment so the xdph screen-share picker
# inherits it too). custom_palette=true is required for qt5ct/qt6ct to
# actually apply the color scheme; Fusion is used because it fully honors a
# custom palette (Breeze/native styles partly ignore it).
"qt5ct/qt5ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${config.home.homeDirectory}/.config/qt5ct/colors/noctalia.conf
color_scheme_path=${pkgs.libsForQt5.qt5ct}/share/qt5ct/colors/darker.conf
'';
"qt6ct/qt6ct.conf".text = ''
[Appearance]
style=Fusion
custom_palette=true
color_scheme_path=${config.home.homeDirectory}/.config/qt6ct/colors/noctalia.conf
color_scheme_path=${pkgs.kdePackages.qt6ct}/share/qt6ct/colors/darker.conf
'';
};
}
+12
View File
@@ -0,0 +1,12 @@
{ pkgs, ... }:
{
home.packages = with pkgs; [
doctl
awscli2
terraform
bruno
coturn
usbutils
ktlint
];
}
+1 -8
View File
@@ -5,8 +5,8 @@
{
imports = [
./codex.nix
./pi.nix
./ralph-claude-code.nix
./t3-code.nix
];
programs = {
@@ -25,19 +25,12 @@
home.packages = with pkgs; [
imagemagick
doctl
awscli2
terraform
inotify-tools
devenv
bruno
sshfs
coturn
file
usbutils
nmap
lsof
ktlint
croc
opencode
process-compose
+3
View File
@@ -19,6 +19,9 @@ tmp/**/*
*.log
*.tmp
# Local git worktree checkouts
worktrees/
# Latex
_inputs.tex
_sections.tex
+6 -4
View File
@@ -1,17 +1,19 @@
{ pkgs, config, ... }:
let
cfg = config.custom.hyprland;
nordzy-cursors = pkgs.callPackage ../../../../pkgs/nordzy-cursors/package.nix { };
cursorSize = config.custom.hyprland.cursorSize;
cursorPackage = if cfg.cursorPackage != null then cfg.cursorPackage else nordzy-cursors;
cursorSize = cfg.cursorSize;
in
{
home.packages = [
nordzy-cursors
cursorPackage
];
home.sessionVariables = {
HYPRCURSOR_THEME = "Nordzy-cursors"; # Dark variant (default)
HYPRCURSOR_THEME = cfg.cursorTheme.dark;
HYPRCURSOR_SIZE = cursorSize;
XCURSOR_THEME = "Nordzy-cursors";
XCURSOR_THEME = cfg.cursorTheme.dark;
XCURSOR_SIZE = cursorSize;
};
+70 -25
View File
@@ -10,6 +10,8 @@ let
hyprPkgs = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system};
osd = "${pkgs.swayosd}/bin/swayosd-client";
# xdph runs this instead of hyprland-share-picker (screencopy:custom_picker_binary).
# The contract is just "print [SELECTION] to stdout"; a leading "r" grants a
# restore token. Printing it immediately means the dialog never renders.
@@ -70,6 +72,41 @@ in
default = 24;
description = "Cursor size for XCURSOR_SIZE and HYPRCURSOR_SIZE";
};
cursorPackage = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = ''
Package providing cursorTheme.dark/light under share/icons. null uses
the nordzy-cursors package built in ./cursor.nix.
'';
};
cursorTheme = {
dark = lib.mkOption {
type = lib.types.str;
default = "Nordzy-cursors";
description = "Cursor theme darkman selects in dark mode.";
};
light = lib.mkOption {
type = lib.types.str;
default = "Nordzy-white";
description = "Cursor theme darkman selects in light mode.";
};
};
panelScale = lib.mkOption {
type = lib.types.float;
default = 1.0;
example = 1.5;
description = "Multiplier for every length in the eww dashboard stylesheet.";
};
verticalOutputs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "DP-2" ];
description = ''
Outputs that get the portrait wallpaper set. Everything else gets the
landscape one.
'';
};
shareOutput = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@@ -84,8 +121,9 @@ in
imports = [
./cursor.nix
./eww
./kbptr.nix
./noctalia
./shell.nix
./wallpapers.nix
];
@@ -204,11 +242,11 @@ in
-- environment
hl.env("GDK_SCALE", "2.0")
hl.env("XCURSOR_SIZE", "${toString cfg.cursorSize}")
hl.env("HYPRCURSOR_THEME", "rose-pine-hyprcursor")
hl.env("HYPRCURSOR_THEME", "${cfg.cursorTheme.dark}")
hl.env("HYPRCURSOR_SIZE", "${toString cfg.cursorSize}")
-- Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so
-- the noctalia-generated color scheme actually applies. Without this
-- var the qt6ct.conf is never read.
-- the dark color scheme actually applies. Without this var the
-- qt6ct.conf is never read.
hl.env("QT_QPA_PLATFORMTHEME", "qt6ct")
-- autostart
@@ -216,16 +254,17 @@ in
-- Import the Qt theme var too, so the dbus/systemd-activated
-- xdg-desktop-portal-hyprland (and its share-picker) inherit it.
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME")
hl.exec_cmd("hyprctl setcursor Nordzy-cursors ${toString cfg.cursorSize}")
hl.exec_cmd("hyprctl setcursor ${cfg.cursorTheme.dark} ${toString cfg.cursorSize}")
hl.exec_cmd("hyprsunset")
hl.exec_cmd("noctalia")
-- eww, wpaperd, mako and the polkit agent are systemd user services
-- bound to graphical-session.target; nothing to start here.
-- kdeconnectd is only DBus-activated; nothing starts it at login,
-- so clipboard sync stays dead until the indicator runs.
hl.exec_cmd("kdeconnect-indicator")
end)
-- layer rules
hl.layer_rule({ match = { namespace = "noctalia-wallpaper" }, blur = true, ignore_alpha = 0.5 })
hl.layer_rule({ match = { namespace = "eww-panel" }, blur = true, ignore_alpha = 0.5 })
-- workspace rules
hl.workspace_rule({ workspace = "w[t1]", gaps_out = 0, gaps_in = 0 }) -- no gaps when only window
@@ -253,7 +292,11 @@ in
})
hl.window_rule({ match = { class = "thunar" }, float = true, size = "1800 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[\.blueman-manager-wrapped]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- nixpkgs wraps blueman with --inherit-argv0, so the class is
-- blueman-manager, not the .blueman-manager-wrapped the old rule expected.
hl.window_rule({ match = { class = [[.*[Bb]lueman.*]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "nm-connection-editor" }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = "waypaper" }, float = true, size = "1600 1000", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
hl.window_rule({ match = { class = [[org\.pulseaudio\.pavucontrol]] }, float = true, size = "1200 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" })
-- wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG).
@@ -276,7 +319,7 @@ in
hl.window_rule({ match = { workspace = "special:yazi" }, center = true })
-- keybinds
hl.bind(mod .. " + N", hl.dsp.exec_cmd("noctalia msg panel-toggle control-center"))
hl.bind(mod .. " + N", hl.dsp.exec_cmd("eww-panel"))
hl.bind(mod .. " + T", hl.dsp.exec_cmd("kitty"))
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mod .. " + Q", hl.dsp.window.close())
@@ -286,7 +329,7 @@ in
-- lock (routes through logind -> hypridle lock_cmd -> hyprlock)
hl.bind(mod .. " + CTRL + L", hl.dsp.exec_cmd("loginctl lock-session"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher"))
hl.bind(mod .. " + space", hl.dsp.exec_cmd("fuzzel"))
-- printscreen
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"))
@@ -324,18 +367,21 @@ in
hl.dispatch(hl.dsp.window.center())
end)
-- volume / brightness (locked so they work on the lock screen; repeat on hold)
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set 5%+"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl set 5%-"), { locked = true, repeating = true })
-- volume / brightness / media (locked so they work on the lock screen;
-- volume and brightness repeat on hold). Everything goes through
-- swayosd-client, which applies the change and draws the OSD popup;
-- store paths because Hyprland execs these with the login PATH, which
-- doesn't pick up profile changes made after the session started.
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${osd} --output-volume raise"), { locked = true, repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("${osd} --output-volume lower"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("${osd} --brightness raise"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("${osd} --brightness lower"), { locked = true, repeating = true })
-- media keys (locked)
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pactl set-sink-mute @DEFAULT_SINK@ toggle"), { locked = true })
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("volumectl -m toggle-mute"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("${osd} --output-volume mute-toggle"), { locked = true })
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("${osd} --input-volume mute-toggle"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("${osd} --playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("${osd} --playerctl prev"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("${osd} --playerctl play-pause"), { locked = true })
-- move/resize windows with mod + LMB/RMB drag
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
@@ -345,10 +391,9 @@ in
services.hyprpaper.enable = false;
# hyprlock: the lock screen, replacing noctalia's built-in locker (which is
# ugly and — unlike hyprlock — opaque to scripts/lock-state detection).
# Clean minimal dark look; tweak colours/clock/font to taste. hypridle below
# drives it (idle + before-sleep), and $mod CTRL+L locks manually.
# hyprlock: the lock screen. Clean minimal dark look; tweak colours/clock/
# font to taste. hypridle below drives it (idle + before-sleep), and
# $mod CTRL+L locks manually.
programs.hyprlock = {
enable = true;
settings = {
@@ -0,0 +1,292 @@
{
config,
lib,
pkgs,
inputs,
...
}:
let
cfg = config.custom.hyprland;
# Every length in the stylesheet goes through this, so one option resizes the
# whole dashboard for 4K hosts.
px = n: "${toString (builtins.ceil (n * cfg.panelScale))}px";
mkScript =
name: runtimeInputs:
pkgs.writeShellApplication {
inherit name runtimeInputs;
text = builtins.readFile (./scripts + "/${name}.sh");
};
battery = mkScript "battery" [
pkgs.coreutils
pkgs.findutils
pkgs.jq
];
network = mkScript "network" [
pkgs.networkmanager
pkgs.gawk
pkgs.gnused
pkgs.jq
];
bluetooth = mkScript "bluetooth" [
pkgs.bluez
pkgs.coreutils
pkgs.findutils
pkgs.gawk
pkgs.jq
];
audio = mkScript "audio" [
pkgs.gawk
pkgs.jq
pkgs.wireplumber
];
panelToggle = mkScript "eww-panel" [
config.programs.eww.package
pkgs.jq
inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland
];
eww = lib.getExe config.programs.eww.package;
bluetoothctl = "${pkgs.bluez}/bin/bluetoothctl";
wpctl = "${pkgs.wireplumber}/bin/wpctl";
wpaperctl = "${pkgs.wpaperd}/bin/wpaperctl";
# setsid, and before the close rather than after it: eww kills the onclick
# handler's process group when the window it came from goes away, so a bare
# `close panel; cmd` never reaches cmd.
act = cmd: "${pkgs.util-linux}/bin/setsid --fork ${cmd}; ${eww} close panel";
in
{
home.packages = [
panelToggle
pkgs.waypaper
];
programs.eww = {
enable = true;
systemd.enable = true;
yuckConfig = ''
;; Each script prints one JSON object; widgets read .icon / .label / .sub
;; off it.
(defpoll clock-time :interval "10s" :initial "--:--" `date +%H:%M`)
(defpoll clock-date :interval "1h" :initial "" `date "+%A, %d %B"`)
(defpoll battery
:interval "30s"
:initial '{"present":false,"icon":"","label":"","sub":""}'
`${lib.getExe battery}`)
(defpoll network
:interval "10s"
:initial '{"icon":"","label":"...","sub":""}'
`${lib.getExe network}`)
(defpoll bluetooth
:interval "10s"
:initial '{"powered":false,"icon":"","label":"...","sub":""}'
`${lib.getExe bluetooth}`)
(defpoll speaker
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} sink`)
(defpoll mic
:interval "2s"
:initial '{"present":false,"volume":0,"muted":true,"icon":""}'
`${lib.getExe audio} source`)
(defwidget card [title]
(box :class "card" :orientation "v" :space-evenly false
(label :class "card-title" :halign "start" :text title :visible {title != ""})
(box :orientation "v" :space-evenly false
(children))))
(defwidget row [icon label sub onclick]
(eventbox :class "row" :cursor "pointer" :onclick onclick
(box :space-evenly false
(label :class "row-icon" :text icon)
(box :orientation "v" :space-evenly false :hexpand true
(label :class "row-label" :halign "start" :text label)
(label :class "row-sub" :halign "start" :text sub :visible {sub != ""})))))
;; Mute button, slider, readout. `state` is the speaker/mic json object.
(defwidget volume [state node]
(box :space-evenly false :visible {state.present}
(button :class {state.muted ? "mute muted" : "mute"}
:onclick "${wpctl} set-mute ''${node} toggle"
{state.icon})
(scale :class "slider" :hexpand true
:value {state.volume} :min 0 :max 101
:onchange "${wpctl} set-volume ''${node} {}%")
(label :class "row-sub" :text {"''${state.volume}%"})))
(defwidget action [icon tooltip onclick]
(button :class "action" :tooltip tooltip :onclick onclick icon))
(defwidget dashboard []
(box :class "dash" :orientation "v" :space-evenly false
(box :space-evenly false
(card :title ""
(label :class "clock-time" :halign "start" :text clock-time)
(label :class "clock-date" :halign "start" :text clock-date))
(card :title "System"
(box :visible {battery.present} :space-evenly false
(row :icon {battery.icon} :label {battery.label}
:sub {battery.sub} :onclick ""))
(row :icon {network.icon} :label {network.label} :sub {network.sub}
:onclick "${act "${pkgs.networkmanagerapplet}/bin/nm-connection-editor"}")
(row :icon {bluetooth.icon} :label {bluetooth.label} :sub {bluetooth.sub}
:onclick "${act "${pkgs.blueman}/bin/blueman-manager"}")))
(box :space-evenly false
(card :title "Audio"
(volume :state speaker :node "@DEFAULT_AUDIO_SINK@")
(volume :state mic :node "@DEFAULT_AUDIO_SOURCE@"))
(card :title "Actions"
(box :space-evenly false :halign "start"
(action :icon "󰑐" :tooltip "Next wallpaper"
:onclick "${wpaperctl} next")
(action :icon "󰸉" :tooltip "Pick wallpaper"
:onclick "${act "${lib.getExe pkgs.waypaper}"}")
(action :icon "󰂯" :tooltip "Toggle bluetooth"
:onclick {bluetooth.powered
? "${bluetoothctl} power off"
: "${bluetoothctl} power on"})
(action :icon "󰌾" :tooltip "Lock"
:onclick "${act "${pkgs.systemd}/bin/loginctl lock-session"}"))))))
;; Sized to its content. A full-screen backdrop would give click-outside-
;; to-close, but its handler also fires for clicks on the sliders.
(defwindow panel
:namespace "eww-panel"
:geometry (geometry :anchor "center")
:stacking "overlay"
(dashboard))
'';
scssConfig = ''
$bg: rgba(46, 52, 64, 0.96);
$card: rgba(59, 66, 82, 0.72);
$border: #4c566a;
$fg: #eceff4;
$muted: #81a1c1;
$accent: #88c0d0;
$red: #bf616a;
* {
all: unset;
font-family: "FiraCode Nerd Font", monospace;
}
window {
background-color: transparent;
}
.dash {
background-color: $bg;
border: ${px 1} solid $border;
border-radius: ${px 20};
padding: ${px 14};
color: $fg;
}
.card {
background-color: $card;
border-radius: ${px 14};
padding: ${px 16};
margin: ${px 6};
min-width: ${px 300};
}
.card-title {
font-size: ${px 11};
color: $muted;
margin-bottom: ${px 8};
}
.clock-time {
font-size: ${px 56};
font-weight: 600;
}
.clock-date {
font-size: ${px 14};
color: $muted;
}
.row {
border-radius: ${px 10};
padding: ${px 10} ${px 12};
&:hover {
background-color: rgba(136, 192, 208, 0.14);
}
}
.row-icon {
font-size: ${px 22};
color: $accent;
min-width: ${px 34};
}
.row-label {
font-size: ${px 14};
}
.row-sub {
font-size: ${px 11};
color: $muted;
}
.mute {
font-size: ${px 20};
color: $accent;
min-width: ${px 34};
&.muted {
color: $red;
}
}
.slider {
margin: 0 ${px 12};
min-width: ${px 180};
trough {
background-color: rgba(76, 86, 106, 0.6);
border-radius: ${px 8};
min-height: ${px 8};
highlight {
background-color: $accent;
border-radius: ${px 8};
}
}
}
.action {
background-color: rgba(76, 86, 106, 0.45);
border-radius: ${px 12};
padding: ${px 12} ${px 18};
margin-right: ${px 8};
font-size: ${px 20};
color: $fg;
&:hover {
background-color: $accent;
color: #2e3440;
}
}
'';
};
}
@@ -0,0 +1,37 @@
# One JSON object per audio node. $1 is "sink" or "source". wpctl prints
# "Volume: 0.45", with " [MUTED]" appended, and errors when there is no node.
case "$1" in
sink) node="@DEFAULT_AUDIO_SINK@" ;;
source) node="@DEFAULT_AUDIO_SOURCE@" ;;
*) echo "usage: audio sink|source" >&2; exit 2 ;;
esac
raw=$(wpctl get-volume "$node" 2>/dev/null || true)
case "$raw" in
*Volume:*) ;;
*)
jq -nc '{ present: false, volume: 0, muted: true, icon: "󰖁" }'
exit 0
;;
esac
volume=$(printf '%s' "$raw" | awk '{ printf "%d", $2 * 100 }')
muted=false
case "$raw" in *MUTED*) muted=true ;; esac
if [ "$1" = "source" ]; then
if [ "$muted" = true ]; then icon="󰍭"; else icon="󰍬"; fi
elif [ "$muted" = true ]; then icon="󰖁"
elif [ "$volume" -ge 66 ]; then icon="󰕾"
elif [ "$volume" -ge 33 ]; then icon="󰖀"
else icon="󰕿"
fi
jq -nc \
--argjson volume "$volume" \
--argjson muted "$muted" \
--arg icon "$icon" \
'{ present: true, volume: $volume, muted: $muted, icon: $icon }'
@@ -0,0 +1,32 @@
# One JSON object for the panel's battery row. `present: false` on the desktops,
# which have no BAT* at all — the row hides itself on that.
bat=$(find /sys/class/power_supply -maxdepth 1 -name 'BAT*' 2>/dev/null | sort | head -1)
if [ -z "$bat" ]; then
jq -nc '{ present: false, icon: "", label: "", sub: "" }'
exit 0
fi
capacity=$(cat "$bat/capacity")
status=$(cat "$bat/status")
if [ "$status" = "Charging" ] || [ "$status" = "Full" ]; then
icon="󰂄"
elif [ "$capacity" -ge 90 ]; then icon="󰁹"
elif [ "$capacity" -ge 80 ]; then icon="󰂂"
elif [ "$capacity" -ge 70 ]; then icon="󰂁"
elif [ "$capacity" -ge 60 ]; then icon="󰂀"
elif [ "$capacity" -ge 50 ]; then icon="󰁿"
elif [ "$capacity" -ge 40 ]; then icon="󰁾"
elif [ "$capacity" -ge 30 ]; then icon="󰁽"
elif [ "$capacity" -ge 20 ]; then icon="󰁼"
elif [ "$capacity" -ge 10 ]; then icon="󰁻"
else icon="󰁺"
fi
jq -nc \
--arg icon "$icon" \
--arg label "$capacity%" \
--arg sub "$status" \
'{ present: true, icon: $icon, label: $label, sub: $sub }'
@@ -0,0 +1,22 @@
# One JSON object for the panel's bluetooth row. bluetoothctl blocks forever
# instead of erroring when there is no adapter, hence the /sys check + timeouts.
if [ -z "$(find /sys/class/bluetooth -mindepth 1 -maxdepth 1 2>/dev/null)" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "No adapter", sub: "" }'
exit 0
fi
powered=$(timeout 2 bluetoothctl show 2>/dev/null | awk '/Powered:/ { print $2; exit }' || true)
if [ "$powered" != "yes" ]; then
jq -nc '{ powered: false, icon: "󰂲", label: "Off", sub: "" }'
exit 0
fi
connected=$(timeout 2 bluetoothctl devices Connected 2>/dev/null | cut -d' ' -f3- | paste -sd', ' - || true)
if [ -n "$connected" ]; then
jq -nc --arg sub "$connected" '{ powered: true, icon: "󰂱", label: "On", sub: $sub }'
else
jq -nc '{ powered: true, icon: "󰂯", label: "On", sub: "Nothing connected" }'
fi
@@ -0,0 +1,11 @@
# Toggle the panel on whichever monitor has focus. `eww open --toggle` can't do
# this: it ignores --screen when the window is already open elsewhere, so the
# panel would stay stuck on the monitor it first appeared on.
if eww active-windows | grep -q '^panel'; then
exec eww close panel
fi
screen=$(hyprctl -j monitors | jq -r 'map(select(.focused))[0].id // 0')
exec eww open panel --screen "$screen"
@@ -0,0 +1,43 @@
# One JSON object for the panel's network row. State must match "connected"
# exactly: NetworkManager reports docker0 and br-* as "connected (externally)".
status=$(nmcli -t -f TYPE,STATE,CONNECTION device status 2>/dev/null || true)
# nmcli's terse output backslash-escapes colons inside connection names, so
# rejoin fields 3..NF and unescape rather than taking $3.
pick() {
printf '%s\n' "$status" | awk -F: -v want="$1" '
$1 == want && $2 == "connected" {
name = $3
for (i = 4; i <= NF; i++) name = name ":" $i
print name
exit
}' | sed 's/\\:/:/g'
}
wifi=$(pick wifi)
ethernet=$(pick ethernet)
if [ -n "$wifi" ]; then
signal=$(nmcli -t -f IN-USE,SIGNAL dev wifi 2>/dev/null | awk -F: '$1 == "*" { print $2; exit }' || true)
if [ -z "$signal" ]; then icon="󰤨"
elif [ "$signal" -ge 75 ]; then icon="󰤨"
elif [ "$signal" -ge 50 ]; then icon="󰤥"
elif [ "$signal" -ge 25 ]; then icon="󰤢"
else icon="󰤟"
fi
if [ -n "$signal" ]; then
sub="Wi-Fi · $signal%"
else
sub="Wi-Fi"
fi
jq -nc --arg icon "$icon" --arg label "$wifi" --arg sub "$sub" \
'{ icon: $icon, label: $label, sub: $sub }'
elif [ -n "$ethernet" ]; then
jq -nc --arg label "$ethernet" '{ icon: "󰈀", label: $label, sub: "Ethernet" }'
else
jq -nc '{ icon: "󰤭", label: "Offline", sub: "" }'
fi
@@ -1,104 +0,0 @@
{
config,
lib,
inputs,
pkgs,
...
}:
let
avatar = pkgs.fetchurl {
url = "https://github.com/naps62.png";
sha256 = "1ck11xg4rwcvn8dcc06ax7i9yfzy21v1n6h5mcjrkrici5sznlsv";
};
in
{
imports = [ inputs.noctalia.homeModules.default ];
# Noctalia v5 rewrote the config schema (TOML, snake_case, flat sections).
# The v4 settings (appLauncher.*, bar.widgets.*, controlCenter.shortcuts.*, etc.)
# have no v5 equivalents — most must now be tweaked through the in-shell
# Settings UI. See docs.noctalia.dev/v5 and example.toml in the upstream repo.
programs.noctalia = {
enable = true;
settings = {
shell = {
font_family = "Sans Serif";
avatar_path = "${config.home.homeDirectory}/.face";
telemetry_enabled = false;
polkit_agent = true;
};
wallpaper = {
enabled = true;
fill_mode = "crop";
directory = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
transition = [ "fade" ];
transition_duration = 1500;
automation = {
enabled = true;
interval_minutes = 360;
order = "random";
recursive = true;
};
};
theme = {
mode = "dark";
source = "builtin";
builtin = "Nord";
templates = {
enable_builtin_templates = true;
builtin_ids = [
"btop"
"gtk3"
"gtk4"
"hyprland"
"qt"
];
};
};
notification.enable_daemon = true;
weather = {
enabled = true;
unit = "celsius";
};
location = {
auto_locate = false;
address = "Braga, Portugal";
};
nightlight = {
enabled = true;
temperature_day = 6500;
temperature_night = 4000;
};
bar.main = {
position = "top";
background_opacity = 0.93;
radius = 12;
start = [ "launcher" "wallpaper" "workspaces" ];
center = [ "media" "clock" ];
end = [ "tray" "notifications" "network" "bluetooth" "volume" "brightness" "battery" "control-center" "session" ];
};
};
};
home.file.".face".source = avatar;
# force overwrite files that noctalia may have turned into regular files
# (Hyprland 0.55+ is Lua-only, so the managed file is now hyprland.lua)
xdg.configFile."hypr/hyprland.lua".force = true;
xdg.configFile."gtk-3.0/gtk.css".force = true;
# NOTE: noctalia still writes its color theme to ~/.config/hypr/noctalia.conf
# in *hyprlang* syntax ($primary = rgb(...), general { col.active_border = ... }).
# Hyprland 0.55+ dropped the hyprlang parser entirely, so that file can no
# longer be `source`d — there is no hl.source, and the lua config can't parse
# hyprlang. Hyprland therefore uses the static border colors from
# ../default.nix instead of noctalia's themed ones. To restore live theming,
# noctalia would need to emit a Lua snippet we can require() here.
}
+84
View File
@@ -0,0 +1,84 @@
{ pkgs, ... }:
{
programs.fuzzel = {
enable = true;
settings = {
main = {
terminal = "${pkgs.kitty}/bin/kitty";
layer = "overlay";
font = "FiraCode Nerd Font:size=12";
icon-theme = "Tela black";
lines = 12;
width = 40;
inner-pad = 8;
};
border = {
radius = 12;
width = 1;
};
colors = {
background = "2e3440f0";
text = "eceff4ff";
match = "88c0d0ff";
selection = "434c5eff";
selection-text = "eceff4ff";
border = "4c566aff";
};
};
};
services.mako = {
enable = true;
settings = {
font = "FiraCode Nerd Font 11";
background-color = "#2e3440f0";
text-color = "#eceff4";
border-color = "#4c566a";
border-radius = 12;
border-size = 1;
padding = "14";
margin = "12";
default-timeout = 6000;
anchor = "top-right";
layer = "overlay";
};
};
# swayosd: the on-screen volume/brightness/caps-lock popup. Noctalia drew this
# before; without it the XF86 keys still work but give no feedback, which reads
# as "the keys are dead". swayosd-client does the change *and* the popup, so the
# binds in default.nix call it instead of wpctl/brightnessctl directly.
services.swayosd = {
enable = true;
topMargin = 0.85;
stylePath = pkgs.writeText "swayosd-style.css" ''
window#osd {
padding: 12px 20px;
border-radius: 12px;
border: 1px solid #4c566a;
background: alpha(#2e3440, 0.94);
}
window#osd #container { margin: 12px; }
window#osd image,
window#osd label { color: #eceff4; }
window#osd progressbar:disabled { opacity: 0.5; }
window#osd trough {
min-height: 6px;
border-radius: 6px;
background: #434c5e;
}
window#osd progress {
min-height: 6px;
border-radius: 6px;
background: #88c0d0;
}
'';
};
services.hyprpolkitagent.enable = true;
}
+27 -1
View File
@@ -1,5 +1,10 @@
{ pkgs, ... }:
{ config, pkgs, ... }:
let
cfg = config.custom.hyprland;
horizontalDir = "${config.home.homeDirectory}/.cache/wallpapers/3840x2160";
verticalDir = "${config.home.homeDirectory}/.cache/wallpapers/2160x3840";
# Horizontal wallpapers (3840x2160) for HDMI-A-1 and DP-1
# Just add any image URL you want here
horizontal = [
@@ -107,4 +112,25 @@ let
in
{
home.file.".local/bin/wallpaper-fetch".source = fetchScript;
# `any` covers every output not named in custom.hyprland.verticalOutputs,
# which get the portrait set instead.
services.wpaperd = {
enable = true;
settings =
{
default = {
duration = "6h";
sorting = "random";
mode = "center";
};
any.path = horizontalDir;
}
// builtins.listToAttrs (
map (output: {
name = output;
value.path = verticalDir;
}) cfg.verticalOutputs
);
};
}
+1 -1
View File
@@ -117,7 +117,7 @@ in
{
programs.kitty = {
enable = true;
# Fixed theme — noctalia no longer toggles kitty with dark/light mode.
# Fixed theme — nothing toggles kitty with dark/light mode.
themeFile = "Catppuccin-Mocha";
settings = {
confirm_os_window_close = 0;
+7
View File
@@ -0,0 +1,7 @@
{ inputs, ... }:
# Maestro: the terminal-session orchestrator. Module only — no `enable` here,
# because the daemon owns every interactive shell on the host it runs on, and
# only one box should be doing that. Hosts opt in from their own services.nix.
{
imports = [ inputs.maestro.homeManagerModules.default ];
}
+36
View File
@@ -0,0 +1,36 @@
{
config,
lib,
...
}:
let
cfg = config.custom.mpv;
in
{
options.custom.mpv = {
hwdec = lib.mkOption {
type = lib.types.str;
default = "auto-safe";
example = "nvdec-copy";
description = ''
Value for mpv's `hwdec`. `auto-safe` only picks a decoder mpv considers
reliable for the running driver and falls back to software otherwise,
so it is correct on Intel (arrakis) and on hosts with no usable GPU
decoder (yolo). NVIDIA hosts override it.
'';
};
};
config = {
programs.mpv = {
enable = true;
config = {
hwdec = cfg.hwdec;
vo = lib.mkDefault "gpu-next";
save-position-on-quit = true;
keep-open = "yes";
};
};
};
}
@@ -0,0 +1,198 @@
{
config,
lib,
pkgs,
...
}:
# nix-autodeploy: one webhook endpoint that turns "a repo I own pushed to main"
# into "that flake input is bumped, committed, and (optionally) applied".
#
# Replaces per-app deploy webhooks that rebuilt from a checkout on the box. The
# apps are flake inputs now, so deploying one is a lock bump plus a generation
# switch — the same operation for every app, hence one service instead of N.
let
cfg = config.services.nixAutodeploy;
# A user unit inherits almost no PATH, and the deploy shells out to git (with
# the gitea credential helper), nix and nh.
profilePath = lib.concatStringsSep ":" [
"%h/.local/bin"
"%h/.nix-profile/bin"
"/etc/profiles/per-user/%u/bin"
"/run/current-system/sw/bin"
];
deploy = pkgs.writeShellApplication {
name = "nix-autodeploy-deploy";
runtimeInputs = [
pkgs.git
pkgs.nix
pkgs.nh
pkgs.curl
pkgs.jq
pkgs.util-linux
];
text = ''
# usage: nix-autodeploy-deploy <flake-input> <apply|notify>
input=$1
mode=$2
flake=${lib.escapeShellArg cfg.flake}
topic=${lib.escapeShellArg cfg.ntfy.topic}
ntfy_url=${lib.escapeShellArg cfg.ntfy.url}
notify() {
[ -n "''${NTFY_TOKEN:-}" ] || return 0
curl -fsS -X POST "$ntfy_url/$topic" \
-H "Authorization: Bearer $NTFY_TOKEN" \
-H "Title: $1" -d "$2" > /dev/null || true
}
# Serialize: two pushes landing together would otherwise race on the same
# working tree and the same flake.lock.
exec 9> "''${XDG_RUNTIME_DIR:-/tmp}/nix-autodeploy.lock"
flock 9
cd "$flake"
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" != main ]; then
notify "autodeploy skipped ($input)" "checkout is on $branch, not main"
exit 0
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
notify "autodeploy skipped ($input)" "working tree is dirty"
exit 0
fi
git fetch --quiet origin main
git merge --ff-only --quiet origin/main
nix flake update "$input"
if git diff --quiet -- flake.lock; then
echo "autodeploy: $input already at the pushed revision"
exit 0
fi
rev=$(nix flake metadata --json |
jq -r --arg i "$input" '.locks.nodes[$i].locked.rev[0:7]')
git commit --quiet -m "chore(flake): bump $input to $rev" -- flake.lock
git push --quiet origin main
if [ "$mode" != apply ]; then
notify "$input bumped to $rev" "Not applied run 'nh home switch' when convenient."
exit 0
fi
if nh home switch "$flake"; then
notify "$input deployed" "Bumped to $rev and switched."
else
notify "$input FAILED to apply" "Lock is at $rev on main; the switch failed. See journalctl --user -u run-*."
exit 1
fi
'';
};
listener = pkgs.writers.writePython3Bin "nix-autodeploy" {
# Only line length: http.server's do_GET/do_POST spelling is already
# excused inline.
flakeIgnore = [ "E501" ];
} (builtins.readFile ./listener.py);
repoFile = (pkgs.formats.json { }).generate "nix-autodeploy-repos.json" (
lib.mapAttrs (_: r: { inherit (r) input apply; }) cfg.repos
);
in
{
options.services.nixAutodeploy = {
enable = lib.mkEnableOption "the forge-webhook listener that bumps and applies flake inputs";
port = lib.mkOption {
type = lib.types.port;
default = 7375;
description = "Port the listener binds on 0.0.0.0. There is no auth beyond the webhook HMAC, so only expose it through the reverse proxy.";
};
flake = lib.mkOption {
type = lib.types.str;
example = "/home/naps62/tea/nixos-config";
description = "Absolute path to the nixos-config checkout whose flake.lock gets bumped. Must be on main and clean, or the deploy skips.";
};
environmentFile = lib.mkOption {
type = lib.types.str;
example = "%h/.config/nix-autodeploy/env";
description = ''
File holding `NIX_AUTODEPLOY_SECRET` (the webhook HMAC secret, shared
with every repo below) and `NTFY_TOKEN`. Not in the store these are
secrets.
'';
};
ntfy = {
url = lib.mkOption {
type = lib.types.str;
default = "https://ntfy.home.naps.pt";
description = "Base URL of the ntfy server deploy results are posted to.";
};
topic = lib.mkOption {
type = lib.types.str;
default = "nix-autodeploy";
description = "ntfy topic for deploy results.";
};
};
repos = lib.mkOption {
default = { };
description = "Forge repositories to listen for, keyed by `<owner>/<repo>` exactly as the webhook payload spells it.";
example = lib.literalExpression ''
{ "yolo/rev" = { input = "rev"; }; }
'';
type = lib.types.attrsOf (
lib.types.submodule {
options = {
input = lib.mkOption {
type = lib.types.str;
description = "Name of the flake input in this repo's flake.nix that tracks that repository.";
};
apply = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to run `nh home switch` after the bump. Set false for an
app whose restart disrupts a live session the lock is still
bumped and pushed, and the ntfy message says it is waiting.
'';
};
};
}
);
};
};
config = lib.mkIf cfg.enable {
systemd.user.services.nix-autodeploy = {
Unit = {
Description = "nix-autodeploy forge webhooks bump and apply flake inputs";
After = [ "network.target" ];
# Same restart-budget trap every other always-on unit here avoids: at
# RestartSec=2 a fast-crashing listener would park in `failed`.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
ExecStart = lib.getExe listener;
EnvironmentFile = cfg.environmentFile;
Environment = [
"PATH=${profilePath}"
"NIX_AUTODEPLOY_PORT=${toString cfg.port}"
"NIX_AUTODEPLOY_DEPLOY_BIN=${lib.getExe deploy}"
"NIX_AUTODEPLOY_ENV_FILE=${cfg.environmentFile}"
"NIX_AUTODEPLOY_REPOS_FILE=${repoFile}"
];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
};
}
@@ -0,0 +1,104 @@
"""Forge webhook listener that turns a push into a flake-input bump.
Verifies the HMAC a Gitea (or GitHub) webhook signs the body with, then hands
the actual work to a transient systemd unit. Nothing is done in-process: the
deploy runs `nh home switch`, which restarts every unit home-manager owns —
including this listener — so the job has to outlive it.
"""
import hashlib
import hmac
import json
import os
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SECRET = os.environ.get("NIX_AUTODEPLOY_SECRET", "").encode()
PORT = int(os.environ.get("NIX_AUTODEPLOY_PORT", "7375"))
DEPLOY = os.environ["NIX_AUTODEPLOY_DEPLOY_BIN"]
# {"<owner>/<repo>": {"input": "rev", "apply": true}, ...}. Passed as a file,
# not a variable: systemd's Environment= strips the quotes out of inline JSON.
with open(os.environ["NIX_AUTODEPLOY_REPOS_FILE"]) as fh:
REPOS = json.load(fh)
ENV_FILE = os.environ.get("NIX_AUTODEPLOY_ENV_FILE", "")
MAX_BODY = 1 << 20
if not SECRET:
sys.exit("NIX_AUTODEPLOY_SECRET is not set")
def signature_ok(body: bytes, headers) -> bool:
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
# Gitea sends the bare hex digest; GitHub prefixes it with "sha256=".
for name in ("X-Gitea-Signature", "X-Hub-Signature-256"):
got = headers.get(name)
if got and hmac.compare_digest(got.removeprefix("sha256="), expected):
return True
return False
def spawn(repo: str, entry: dict) -> None:
cmd = [
"systemd-run",
"--user",
"--collect",
f"--description=nix-autodeploy: {repo}",
# PATH is not inherited by a transient unit, and the deploy shells out
# to git, nix and the gitea credential helper.
f"--setenv=PATH={os.environ['PATH']}",
]
if ENV_FILE:
cmd.append(f"--property=EnvironmentFile={ENV_FILE}")
cmd += [DEPLOY, entry["input"], "apply" if entry.get("apply", True) else "notify"]
subprocess.run(cmd, check=True)
class Handler(BaseHTTPRequestHandler):
def reply(self, code: int, text: str) -> None:
payload = text.encode()
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's spelling
if self.path == "/health":
self.reply(200, "ok\n")
else:
self.reply(404, "no\n")
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
if length > MAX_BODY:
return self.reply(413, "body too large\n")
body = self.rfile.read(length)
if not signature_ok(body, self.headers):
return self.reply(401, "bad signature\n")
try:
event = json.loads(body)
except json.JSONDecodeError:
return self.reply(400, "bad json\n")
ref = event.get("ref")
repo = (event.get("repository") or {}).get("full_name")
if ref != "refs/heads/main":
return self.reply(200, f"ignored ref {ref}\n")
entry = REPOS.get(repo)
if entry is None:
return self.reply(200, f"ignored repo {repo}\n")
spawn(repo, entry)
self.reply(202, f"deploying {entry['input']}\n")
def log_message(self, fmt: str, *args) -> None:
# Journal already timestamps; the default format prepends its own.
sys.stderr.write(f"{self.address_string()} {fmt % args}\n")
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
-1
View File
@@ -1 +0,0 @@
Screenshots: stored in ~/downloads/screenshots, with date time in the filename
@@ -1,100 +0,0 @@
---
description: Merge worktree changes and clean up safely
---
# Merge Worktree and Cleanup
Merge changes from the current worktree into a target branch and clean up the worktree.
**Usage:** `/merge [target-branch]`
**Arguments:**
- `$1`: Target branch to merge into (default: auto-detect main/master)
## Process
### 1. Pre-flight Checks
```bash
CURRENT_BRANCH=$(git branch --show-current)
WORKTREE_DIR=$(git rev-parse --git-common-dir)
if [[ ! "$WORKTREE_DIR" == *".git/worktrees"* ]]; then
echo "❌ Error: You're not in a worktree"
exit 1
fi
if ! git diff-index --quiet HEAD --; then
echo "❌ Error: You have uncommitted changes"
git status --short
exit 1
fi
```
### 2. Determine Target Branch
```bash
TARGET_BRANCH="${1:-}"
if [ -z "$TARGET_BRANCH" ]; then
if git show-ref --verify --quiet refs/heads/main; then
TARGET_BRANCH="main"
elif git show-ref --verify --quiet refs/heads/master; then
TARGET_BRANCH="master"
else
echo "❌ Error: Cannot auto-detect main branch"
exit 1
fi
fi
```
### 3. Update Target Branch
```bash
WORKTREE_PATH=$(pwd)
COMMON_DIR=$(git rev-parse --git-common-dir)
REPO_ROOT=$(echo "$COMMON_DIR" | sed 's/\.git.*//' | sed 's/\/$//')
if [ "$COMMON_DIR" = ".git" ]; then
REPO_ROOT=$(git rev-parse --show-toplevel)
fi
cd "$REPO_ROOT"
git checkout "$TARGET_BRANCH"
git pull origin "$TARGET_BRANCH"
```
### 4. Merge
```bash
git merge --no-ff "$CURRENT_BRANCH" -m "Merge branch '$CURRENT_BRANCH'"
```
If you prefer a squash merge, do this instead:
```bash
git merge --squash "$CURRENT_BRANCH"
git commit -m "feat: <summary>"
```
### 5. Push and Clean Up
```bash
git push origin "$TARGET_BRANCH"
git worktree remove "$WORKTREE_PATH" --force || rm -rf "$WORKTREE_PATH"
git branch -d "$CURRENT_BRANCH" || git branch -D "$CURRENT_BRANCH"
```
### 6. Optional: Delete Remote Branch
```bash
git push origin --delete "$CURRENT_BRANCH"
```
## Language-specific checks
### Rust
- `cargo check`
- `cargo clippy` (clean up new warnings)
### TypeScript
- `tsc`
@@ -1,9 +0,0 @@
---
description: Rebase/merge upstream changes into the current branch
---
This command should only work while in a secondary branch (not main or master).
Commit any unstaged changes
Fetch updates from the origin repo
If there are new commits on the parent branch (typically origin/main), then merge them back into the current branch
Solve any conflicts, and analyze the incoming changes to see if additional changes are required to the branch's code (e.g.: if something was renamed in the meantime in main, our new code may need to be adjusted)
+5 -6
View File
@@ -1,12 +1,13 @@
_:
{
# Global rules (opencode/AGENTS.md) and the shared commands (merge, update)
# come from the agent-skills home-manager module — opencode has no @file
# imports, so the module concatenates the shared fragments into one file.
# opencode also auto-loads skills from ~/.claude/skills and ~/.agents/skills.
xdg.configFile = {
# Global config file
"opencode/opencode.json".source = ./opencode.json;
# Global rules (equivalent to ~/.claude/CLAUDE.md)
"opencode/AGENTS.md".source = ./AGENTS.md;
# Custom agents
"opencode/agents/oracle.md".source = ./agents/oracle.md;
"opencode/agents/explorer.md".source = ./agents/explorer.md;
@@ -16,10 +17,8 @@ _:
"opencode/agents/designer-bold.md".source = ./agents/designer-bold.md;
"opencode/agents/analyze-branch.md".source = ./agents/analyze-branch.md;
# Global commands
# Global commands (opencode-specific ones only)
"opencode/commands/work.md".source = ./commands/work.md;
"opencode/commands/merge.md".source = ./commands/merge.md;
"opencode/commands/update.md".source = ./commands/update.md;
"opencode/commands/smart-debug.md".source = ./commands/smart-debug.md;
"opencode/commands/tdd-cycle.md".source = ./commands/tdd-cycle.md;
"opencode/commands/security-scan.md".source = ./commands/security-scan.md;
+12
View File
@@ -0,0 +1,12 @@
{ pkgs, ... }:
let
pi = pkgs.callPackage ../../../pkgs/pi/package.nix { };
in
{
# ~/.pi/agent/AGENTS.md comes from the agent-skills home-manager module
# (pi has no @file imports, so the module concatenates the shared fragments).
# Pi reads skills straight from ~/.agents/skills, which the module links.
# Settings stay unmanaged: pi writes ~/.pi/agent/settings.json itself from
# /settings, /trust and `pi config`.
home.packages = [ pi ];
}
+7
View File
@@ -0,0 +1,7 @@
{ inputs, ... }:
# rev: the always-on code review server. Module only — no `enable` here. It
# discovers every repo under its roots and serves them without auth, so only a
# box that is already a trusted single-user machine should run it.
{
imports = [ inputs.rev.homeManagerModules.default ];
}
-16
View File
@@ -1,16 +0,0 @@
{
config,
pkgs,
lib,
...
}:
let
# T3 Code: open-source desktop control plane for coding agents (x86_64 only).
# Shares the custom.aiApps.deviceScaleFactor knob (defined in ../claude).
t3-code = pkgs.callPackage ../../../pkgs/t3-code/package.nix {
inherit (config.custom.aiApps) deviceScaleFactor;
};
in
{
home.packages = lib.optional pkgs.stdenv.hostPlatform.isx86_64 t3-code;
}
+1
View File
@@ -30,6 +30,7 @@ set -ag terminal-overrides ",xterm*:Tc:smcup@:rmcup@"
# Forward Shift+Enter and other modified keys through to applications
set -s extended-keys on
set -s extended-keys-format csi-u
set -as terminal-features "tmux-256color:extkeys"
# -------------------------------------------------------------------
+20
View File
@@ -0,0 +1,20 @@
{ pkgs, ... }:
{
imports = [ ./desktop/spicetify.nix ];
home.packages = with pkgs; [
obsidian
obs-studio
kooha
vokoscreen-ng
gimp
font-manager
xournalpp
remmina
slack
signal-desktop
zoom-us
yaak
bun
];
}
+12 -1
View File
@@ -10,7 +10,18 @@
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
completionInit = "autoload -U compinit && compinit -u";
# A full compinit security-checks and rebuilds the dump on every shell
# start. Do that at most once a day and use the cached dump (-C) in
# between. Missing dump globs to nothing, so it takes the full path.
completionInit = ''
autoload -U compinit
_zcompdump_fresh() {
setopt local_options extendedglob
[[ -n ''${ZDOTDIR:-$HOME}/.zcompdump(#qNmh-24) ]]
}
if _zcompdump_fresh; then compinit -C; else compinit -u; fi
unfunction _zcompdump_fresh
'';
shellAliases = {
c = "cargo";
+21 -9
View File
@@ -5,6 +5,10 @@
imports = [
../common/programs/default.nix
../common/programs/desktop
../common/programs/workstation-apps.nix
../common/programs/editors.nix
../common/programs/dev-workstation.nix
../common/programs/ai-gui.nix
../common/programs/zen-browser.nix
../common/programs/hyprland
../common/programs/kitty
@@ -20,15 +24,31 @@
home.sessionVariables = {
LIBVA_DRIVER_NAME = "nvidia";
GDM_BACKEND = "nvidia-drm";
# Was GDM_BACKEND, which nothing reads (SDDM is the DM here).
GBM_BACKEND = "nvidia-drm";
__GLX_VENDOR_LIBRARY_NAME = "nvidia";
# Firefox/Zen decode video in a separate RDD process whose sandbox denies
# /dev/nvidia*, so nvidia-vaapi-driver never initialises there and playback
# silently drops to software. Costs one sandbox layer; no narrower switch
# exists. Check with `nvidia-smi --query-gpu=utilization.decoder`.
MOZ_DISABLE_RDD_SANDBOX = "1";
};
# Plain `nvdec` keeps frames in GPU memory and breaks some filters.
custom.mpv.hwdec = "nvdec-copy";
# The 4060 would render Cycles far faster, but nothing caches this build —
# flipping it on compiles Blender, OpenUSD, OpenSubdiv and OpenImageDenoise.
custom.blender.cuda = false;
custom.hyprland = {
yaziSize = "2400 1800";
cursorSize = 42;
# The screen actually shared in every call; skips the xdph picker entirely.
shareOutput = "HDMI-A-1";
verticalOutputs = [ "DP-2" ];
panelScale = 1.6;
};
custom.gaming = {
@@ -47,7 +67,6 @@
-- session the moment Hyprland starts so a cold boot lands on hyprlock, not
-- an open desktop. A brief flash before it paints is possible.
hl.exec_cmd("hyprlock")
hl.exec_cmd("$HOME/.local/bin/hyprpaper-rotate")
end)
-- ethui-dev
@@ -110,12 +129,5 @@
hl.config({ render = { direct_scanout = false } })
'';
programs.noctalia.settings = {
shell.ui_scale = 1.25;
notification.monitors = [ "DP-1" ];
# NOTE: v5 has no per-monitor wallpaper directories; only directory_light /
# directory_dark. Re-add via Settings UI if upstream gains support.
};
programs.kitty.settings.font_size = 16;
}
-141
View File
@@ -1,141 +0,0 @@
default_profile = "default"
[acp]
allow_agent_install = false
allowed_agents = []
auto_stop_idle_secs = 0
compaction_reminder = false
compaction_reminder_percent = 75
default_agent = "claude"
max_concurrent_workers = 5
node_path = ""
offer_structured_in_new_session = false
rate_limit_auto_resume = false
replay_events = 0
restrict_agents = false
show_tool_durations = true
silent_orphan_grace_secs = 120
[auth]
persist_sessions = true
[diff]
context_lines = 3
split_view = false
[hooks]
[host_hooks]
[logging]
default_level = "info"
file_path = "debug.log"
keep_count = 5
max_size_mib = 50
output = "file"
rotation = "size"
show_spans = false
[logging.targets]
[sandbox]
auto_cleanup = true
container_runtime = "docker"
default_image = "ghcr.io/agent-of-empires/aoe-sandbox:latest"
default_terminal_mode = "host"
enabled_by_default = false
environment = [
"TERM",
"COLORTERM",
"FORCE_COLOR",
"NO_COLOR",
]
extra_volumes = []
mount_ssh = false
selinux_relabel = false
volume_ignores = []
volume_ignores_strategy = "anonymous"
[session]
agent_status_hooks = true
auto_resume_on_restart = true
auto_stop_idle_secs = 0
click_action = "live_send"
confirm_before_quit = false
confirm_delete = false
conversation_summary = false
default_attach_mode = "tmux"
delete_to_trash = false
favorites_first = true
inherit_host_environment = false
live_send_exit_chord = "C-q"
live_send_leader = "C-b"
live_send_on_view_switch = false
merge_hooks_into_selected_agent = true
mouse_capture = true
opencode_preassign_session_id = false
prevent_sleep_idle_grace_minutes = 15
prevent_sleep_when_active = false
restart_wake_message = "wake up: pick up what you were doing"
row_tag = "none"
show_session_colors = true
show_tips = true
smart_rename = true
smart_rename_agent = ""
snooze_duration_minutes = 30
strict_hotkeys = false
tie_workdir_to_name = true
trash_retention_days = 30
unread_indicator = true
yolo_mode_default = true
[session.agent_detect_as]
kimiclaude = "claude"
synclaude = "claude"
[session.custom_agents]
kimiclaude = "kimiclaude"
synclaude = "synclaude"
[skills]
auto_propagate = false
[sound]
enabled = false
[status_hooks]
enabled = false
[telemetry]
enabled = false
[theme]
color_mode = "truecolor"
idle_decay_minutes = 0
name = ""
[tmux]
clipboard = "auto"
mouse = "auto"
status_bar = "auto"
vt_live = true
[updates]
auto_update_plugins = false
update_check_mode = "notify"
[web]
notifications_enabled = true
notify_on_error = true
notify_on_idle = false
notify_on_waiting = true
notify_on_wake_fire = true
[worktree]
auto_cleanup = true
bare_repo_path_template = "./{branch}"
delete_branch_on_cleanup = false
enabled = true
init_submodules = true
path_template = "../{repo-name}-worktrees/{branch}"
workspace_path_template = "../{branch}-workspace-{session-id}"
-247
View File
@@ -1,247 +0,0 @@
{
"permissions": {
"allow": [
"Bash",
"Edit",
"Write",
"NotebookEdit",
"WebFetch",
"WebSearch"
],
"deny": [
"Read(~/.ssh/**)",
"Read(*.pem)",
"Read(*.key)",
"mcp__dokploy__application-one",
"mcp__dokploy__mounts-listByServiceId",
"mcp__dokploy__mounts-one",
"mcp__dokploy__compose-one",
"mcp__dokploy__schedule-create",
"mcp__dokploy__schedule-update",
"mcp__dokploy__schedule-runManually"
],
"defaultMode": "auto"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/comment-lint.py"
}
]
},
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/tmux-attention.sh"
}
]
},
{
"matcher": "permission_prompt|elicitation_dialog|agent_needs_input",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf waiting > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
},
{
"matcher": "idle_prompt|agent_completed",
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/comms-lint.py"
}
]
},
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/tmux-reset.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "rtk hook claude"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; IN=$(cat 2>/dev/null); S=running; case \"$IN\" in *\\\"tool_name\\\":\\\"AskUserQuestion\\\"*) S=waiting ;; esac; printf %s \"$S\" > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/git-autoupdate.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/aoe-register-remote.py"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "/home/naps62/tea/rev/scripts/rev-hook-session-start.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c '[ -n \"$AOE_INSTANCE_ID\" ] || exit 0; command -v aoe >/dev/null 2>&1 || exit 0; aoe __extract-session-id 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/home/naps62/tea/rev/scripts/rev-hook-stop.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "sh -c '[ -n \"$AOE_INSTANCE_ID\" ] || exit 0; command -v aoe >/dev/null 2>&1 || exit 0; aoe __extract-session-id 2>/dev/null; exit 0 # aoe-hooks'"
},
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf idle > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
],
"ElicitationResult": [
{
"hooks": [
{
"type": "command",
"command": "sh -c 'unset IFS; set -f; umask 077; [ -n \"$AOE_INSTANCE_ID\" ] || exit 0; case \"$AOE_INSTANCE_ID\" in *[!0-9a-zA-Z_-]*) exit 0 ;; esac; B=/tmp/aoe-hooks-1000; mkdir -p \"$B\" 2>/dev/null || exit 0; LS=$(LC_ALL=C ls -ldn \"$B\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; ME=$(id -u 2>/dev/null) || exit 0; [ \"$3\" = \"$ME\" ] || exit 0; D=\"$B/$AOE_INSTANCE_ID\"; mkdir -p \"$D\" 2>/dev/null; LS=$(LC_ALL=C ls -ldn \"$D\" 2>/dev/null) || exit 0; set -- $LS; M=\"$1\"; case \"$M\" in drwx------|drwx------.|drwx------+|drwx------@) ;; *) exit 0 ;; esac; [ \"$3\" = \"$ME\" ] || exit 0; printf running > \"$D/status\" 2>/dev/null; exit 0 # aoe-hooks'"
}
]
}
]
},
"worktree": {
"baseRef": "fresh"
},
"enabledPlugins": {
"building@subvisual": true,
"rust-analyzer-lsp@claude-plugins-official": false,
"caveman@caveman": true
},
"extraKnownMarketplaces": {
"subvisual": {
"source": {
"source": "github",
"repo": "subvisual/harness"
}
},
"superpowers-marketplace": {
"source": {
"source": "github",
"repo": "obra/superpowers-marketplace"
}
},
"caveman": {
"source": {
"source": "github",
"repo": "JuliusBrussee/caveman"
}
}
},
"skipDangerousModePermissionPrompt": true,
"theme": "dark",
"editorMode": "vim",
"preferredNotifChannel": "auto",
"autoCompactEnabled": true,
"remoteControlAtStartup": false,
"inputNeededNotifEnabled": true,
"mcpServers": {
"playwright": {
"args": [
"-y",
"@playwright/mcp@latest"
],
"command": "npx",
"type": "stdio"
}
},
"sandbox": {
"enabled": false
}
}
+33 -31
View File
@@ -4,14 +4,12 @@
...
}:
let
claudeSettings = "home/yolo/claude-settings.json";
# ./claude-settings.json holds only what yolo overrides; everything else is
# inherited so common changes reach this host. Attrsets merge key-by-key,
# lists are replaced whole (permissions.allow is yolo's, not a union).
mergedClaudeSettings = (pkgs.formats.json { }).generate "claude-settings.json" (
lib.recursiveUpdate (lib.importJSON ../common/programs/claude/settings.json) (
lib.importJSON ./claude-settings.json
# ./opencode.json holds only yolo's overrides; everything else is inherited
# so agents, commands and skills keep coming from common. Attrsets merge
# key-by-key, lists are replaced whole.
mergedOpencodeConfig = (pkgs.formats.json { }).generate "opencode.json" (
lib.recursiveUpdate (lib.importJSON ../common/programs/opencode/opencode.json) (
lib.importJSON ./opencode.json
)
);
in
@@ -23,39 +21,43 @@ in
../common/programs/kitty
../common/programs/gpg.nix
../common/programs/aoe
../common/programs/maestro
../common/programs/rev
../common/programs/nix-autodeploy
../common/features/xdg.nix
./monitors.nix
./services.nix
./ssh.nix
];
# Host-local: bash goes from "ask" to "allow" so unattended opencode sessions
# stop stalling on every git and grep.
# It also drops the prompt on branches under review, which is the tradeoff.
xdg.configFile."opencode/opencode.json".source = lib.mkForce mergedOpencodeConfig;
custom.hyprland.cursorSize = 32;
home = {
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
packages = [ pkgs.agent-browser ];
# Both default to ~/projects/nixos-config in common/programs; this clone
# lives under ~/tea. nh.flake sets NH_FLAKE, so without it `nh home switch`
# with no argument resolves to a path that does not exist.
mutableFilesRepoPath = "/home/naps62/tea/nixos-config";
mutableFiles = {
# Host-local, not shared: this sets yolo_mode_default = true, which starts
# aoe sessions with permission checks skipped. Only correct on this box.
".config/agent-of-empires/config.toml".source = ./aoe-config.toml;
# Likewise host-local: carries skipDangerousModePermissionPrompt and the
# rev hook paths, neither of which belong on a workstation.
".claude/settings.json" = {
source = lib.mkForce mergedClaudeSettings;
upstreamPath = claudeSettings;
};
};
# Amber, and a different silhouette to Nordzy — this desktop is only ever seen
# inside a Moonlight window, so the cursor has to be tellable at a glance from
# the client's own. Same theme in both modes; darkman would otherwise swap it.
custom.hyprland.cursorPackage = pkgs.bibata-cursors;
custom.hyprland.cursorTheme = {
dark = "Bibata-Modern-Amber";
light = "Bibata-Modern-Amber";
};
programs.agentSkills.machine = "yolo";
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
home.packages = [ pkgs.agent-browser ];
# This is the one box that runs the agent-skills units; each starts a session,
# so a second machine enabling them would run the same job twice.
programs.agentSkills = {
machine = "yolo";
prDaemon.enable = true;
hourlog.enable = true;
weekReview.enable = true;
};
programs.nh.flake = "/home/naps62/tea/nixos-config";
# Idle lock and dpms-off blank the virtual output: Sunshine then captures a
+5 -1
View File
@@ -4,7 +4,11 @@ _: {
# Matches every output rather than naming one: the virtio-gpu connector name
# varies by qemu display backend (Virtual-1 vs Virtual-0). Explicit mode, not
# `preferred` — this is the resolution Sunshine streams.
#
# 2160p is near the ceiling: virtio-gpu has no NVENC/VAAPI, so Sunshine falls
# back to x264, which measured 73fps here against 135fps at 1440p — a 60fps
# client has only ~20% margin.
wayland.windowManager.hyprland.extraConfig = ''
hl.monitor({ output = "", mode = "2560x1440@60", position = "0x0", scale = 1 })
hl.monitor({ output = "", mode = "3840x2160@60", position = "0x0", scale = 1 })
'';
}
+5
View File
@@ -0,0 +1,5 @@
{
"permission": {
"bash": "allow"
}
}
+58 -112
View File
@@ -3,12 +3,11 @@
inputs,
...
}:
# The user services this box exists to run, ported from hand-written units in
# ~/.config/systemd/user on the Ubuntu machine.
# The user services this box exists to run.
#
# NOT self-contained: every ExecStart under ~/.bun or ~/.local/bin is an
# imperatively-installed binary, and the WorkingDirectories are clones of
# separate repos. Nix owns the unit definitions here, nothing more.
# maestro and rev come from their own flakes, so nix owns the build as well as
# the unit. aoe-web is still the odd one out: its unit is defined here and the
# binary comes from the flake input.
let
# A user unit gets almost no PATH by default; these are the profile dirs the
# original units got for free from the system PATH on Ubuntu.
@@ -19,133 +18,80 @@ let
aoe = inputs.agent-of-empires.packages.${pkgs.system}.aoe-with-web;
in
{
# sem is here as well as on rev's unit: the shell uses it directly too.
home.packages = [
sem
pkgs.bun
# ACP adapter aoe's structured (web) sessions spawn as `claude-agent-acp`.
pkgs.claude-agent-acp
];
# maestro's and rev's units come from their flake modules, not from the
# hand-written set below.
#
# settings stays empty on purpose: ~/.config/maestro/config.toml is a
# hand-edited plain file, not nix-generated (module skips it iff `{}`).
# There is no `web.enable`: the daemon serves the UI on `daemon.bind_addr`.
services.maestro.enable = true;
# Everything under ~, three levels deep — the worktrees live at
# ~/<area>/<repo>/worktrees/<name>. sem gives entity-level diffs; without it
# rev falls back to line diffs.
services.rev = {
enable = true;
roots = [ "%h" ];
depth = 3;
semBin = "${sem}/bin/sem";
};
# One endpoint for every repo this config pins. All three apply straight
# away: a maestro restart drops the daemon but not the sessions it manages,
# so the interactive shells on this box survive it.
services.nixAutodeploy = {
enable = true;
flake = "/home/naps62/tea/nixos-config";
environmentFile = "%h/.config/nix-autodeploy/env";
repos = {
"yolo/rev".input = "rev";
"yolo/agent-skills".input = "agent-skills";
"naps62/maestro".input = "maestro";
};
};
systemd.user.services = {
rev = {
aoe-web = {
Unit = {
Description = "rev always-on local code review server";
Description = "aoe serve Agent of Empires web dashboard";
After = [ "network.target" ];
# MUST stay 0: at RestartSec=2 a fast-crashing rev burns the default
# MUST stay 0: at RestartSec=2 a fast-crashing aoe burns the default
# 5-starts-per-10s budget, and systemd parks the unit in `failed` until
# a manual `systemctl --user reset-failed`.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h/tea/rev";
# nodejs_26, not pkgs.nodejs: rev's package.json sets engines >=26 and
# the nixpkgs default is 24.
ExecStart = "${pkgs.nodejs_26}/bin/node server/index.ts";
WorkingDirectory = "%h";
# The fork removed dashboard auth entirely, so there is no --auth flag
# any more — the reverse proxy is the only access gate. --allowed-host
# is what makes the rebinding gate accept a hostname under a wildcard
# bind (an IP literal needs no flag).
ExecStart = "${aoe}/bin/aoe serve --host 0.0.0.0 --port 8080 --behind-proxy --allowed-host aoe.n62.casa";
# TMUX_TMPDIR keeps the daemon on the same tmux server the shell and
# TUI use, instead of a second one under /tmp (same bug pr-daemon had).
Environment = [
"NODE_ENV=production"
"REV_ROOTS=%h"
"REV_DEPTH=3"
"REV_SEM_BIN=${sem}/bin/sem"
"PATH=${toolPath}"
"TMUX_TMPDIR=%t"
];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
rev-deploy = {
Unit = {
Description = "rev-deploy Gitea webhook listener that deploys rev on push to main";
After = [ "network.target" ];
# Same restart-budget trap as `rev` above.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h/tea/rev";
ExecStart = "${pkgs.bun}/bin/bun scripts/deploy-webhook.ts";
EnvironmentFile = "%h/.config/rev/deploy.env";
Environment = [ "PATH=${toolPath}" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
aoe-web = {
Unit = {
Description = "aoe serve Agent of Empires web dashboard";
After = [ "network.target" ];
# Same restart-budget trap as `rev` above.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h";
# aoe refuses `--auth none` on a non-loopback bind unless --behind-proxy
# is set; --allowed-host is what makes the rebinding gate accept a
# hostname under a wildcard bind (an IP literal needs no flag).
ExecStart = "${aoe}/bin/aoe serve --host 0.0.0.0 --port 8080 --auth none --behind-proxy --allowed-host aoe.n62.casa";
Environment = [ "PATH=${toolPath}" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
hourlog = {
Unit = {
Description = "Start the Friday hour log in a tmux session";
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
ConditionPathIsDirectory = "%h/tea/agent-skills";
};
Service = {
Type = "oneshot";
ExecStart = "%h/tea/agent-skills/bin/hourlog-session.sh";
Environment = [ "PATH=${toolPath}" ];
# This unit may be what starts the tmux server; the default cgroup kill
# would take it back down as soon as ExecStart returns.
KillMode = "process";
};
};
week-review = {
Unit = {
Description = "Start the weekly agent-skills review in a tmux session";
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
ConditionPathIsDirectory = "%h/tea/agent-skills";
};
Service = {
Type = "oneshot";
ExecStart = "%h/tea/agent-skills/bin/week-review-session.sh";
Environment = [ "PATH=${toolPath}" ];
# If this unit boots before any shell, the shared tmux server lands in
# its cgroup; the default control-group kill would take every session
# down on restart.
KillMode = "process";
};
Install.WantedBy = [ "default.target" ];
};
};
systemd.user.timers = {
hourlog = {
Unit.Description = "Friday hour log, 18:00 Europe/Lisbon";
Timer = {
# Zone suffix pinned because the machine clock is UTC; keeps it at 18:00
# wall time across DST.
OnCalendar = "Fri 18:00 Europe/Lisbon";
Persistent = true;
AccuracySec = "1min";
};
Install.WantedBy = [ "timers.target" ];
};
week-review = {
Unit.Description = "Weekly agent-skills review, Fridays 17:00 Europe/Lisbon";
Timer = {
OnCalendar = "Fri 17:00 Europe/Lisbon";
Persistent = true;
AccuracySec = "1min";
};
Install.WantedBy = [ "timers.target" ];
};
};
}
# pr-daemon, hourlog and week-review are not here: their units ship with the
# scripts they run, in the agent-skills module. This host opts in with
# programs.agentSkills.*.enable in home/yolo/default.nix.
+10
View File
@@ -3,6 +3,16 @@ _:
# on the Ubuntu box.
{
programs.ssh.settings = {
# gpg-agent serves as the ssh agent here, and signing blocks forever on a
# pinentry prompt that has no TTY in headless/agent sessions — `ssh-add -l`
# works, but git push hangs right after "Server accepts key". The key file
# has no passphrase, so read it directly and skip the agent.
"github.com" = {
IdentityAgent = "none";
IdentitiesOnly = true;
IdentityFile = "~/.ssh/id_ed25519";
};
"grafana" = {
HostName = "10.6.10.30";
User = "root";
+6
View File
@@ -35,6 +35,12 @@
share = "media";
mountPoint = "/mnt/media";
}
{
# yolo VM's /home/naps62 (share defined in hosts/yolo/default.nix).
server = "10.7.10.2";
share = "home";
mountPoint = "/mnt/yolo";
}
];
boot.kernelParams = [
+2 -5
View File
@@ -1,7 +1,4 @@
{ pkgs, ... }:
let
sddm-noctalia-theme = pkgs.callPackage ../../../../pkgs/sddm-noctalia-theme/package.nix { };
in
{
services.xserver = {
enable = true;
@@ -15,7 +12,7 @@ in
services.displayManager.sddm = {
enable = true;
wayland.enable = true;
theme = "sddm-noctalia";
theme = "sddm-astronaut-theme";
package = pkgs.kdePackages.sddm;
extraPackages = with pkgs.kdePackages; [
qtmultimedia
@@ -26,6 +23,6 @@ in
};
environment.systemPackages = with pkgs; [
sddm-noctalia-theme
sddm-astronaut
];
}
@@ -10,6 +10,13 @@
portalPackage = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.xdg-desktop-portal-hyprland;
};
# swayosd (the volume/brightness OSD, configured in home) writes
# /sys/class/backlight/*/brightness directly. The package ships a udev rule
# that chgrps those files to `video`; without both the rule and the group,
# brightness keys pop the OSD but change nothing.
services.udev.packages = [ pkgs.swayosd ];
users.users.naps62.extraGroups = [ "video" ];
# GTK/GNOME services still useful on Wayland
programs.dconf.enable = true;
+28
View File
@@ -0,0 +1,28 @@
{ inputs, lib, ... }:
{
imports = [ inputs.nix-flatpak.nixosModules.nix-flatpak ];
services.flatpak = {
enable = true;
# mkOptionDefault so the module's default `flathub` remote is kept rather
# than replaced.
remotes = lib.mkOptionDefault [
{
name = "flathub-beta";
location = "https://flathub.org/beta-repo/flathub-beta.flatpakrepo";
}
];
packages = [
# PrusaSlicer 3.x. As of 3.0 Prusa ships no Linux binary at all — Flathub
# is the only channel, and the alphas live in flathub-beta. Runs alongside
# pkgs.prusa-slicer (2.9.x, in home/common/programs/3d.nix): 3.x keeps its
# profiles in a separate PrusaSlicer3-dev config dir.
{
appId = "com.prusa3d.PrusaSlicer";
origin = "flathub-beta";
}
];
};
}
@@ -47,6 +47,11 @@
# the module's own systemd.user.services.sunshine definition.
systemd.user.services.sunshine.environment.LD_LIBRARY_PATH = "/run/opengl-driver/lib";
# hardware.uinput (pulled in by services.sunshine) makes /dev/uinput
# root:uinput 0660 and adds nobody to the group, so sunshine's virtual mouse
# and keyboard die with "Permission denied" and the stream takes no input.
users.groups.uinput.members = [ "naps62" ];
# DualSense/DualShock emulation. Sunshine presents a *real* PS5 HID (touchpad,
# motion, LEDs, rumble) via /dev/uhid, not plain uinput — a uinput pad can't
# carry those features. The kernel uhid node is root-only (0600) with no udev
+5
View File
@@ -11,6 +11,11 @@ _:
# moonlight shows a black, artefacted picture rather than any error.
systemd.user.services.sunshine.serviceConfig.LimitNOFILE = 65536;
# hardware.uinput (pulled in by services.sunshine) makes /dev/uinput
# root:uinput 0660 and adds nobody to the group, so sunshine's virtual mouse
# and keyboard die with "Permission denied" and the stream takes no input.
users.groups.uinput.members = [ "naps62" ];
services.sunshine = {
enable = true;
autoStart = true;
+4 -1
View File
@@ -40,7 +40,10 @@ in
};
mountPoint = lib.mkOption {
type = lib.types.str;
description = "Local mount point, e.g. \"/mnt/retroarch\".";
# MUST NOT live under $HOME: starship and eww stat every entry of the
# home dir, so a mount point there triggers the automount on every
# prompt and blocks for mount-timeout while the VPN is down.
description = "Local mount point outside $HOME, e.g. \"/mnt/retroarch\".";
};
user = lib.mkOption {
type = lib.types.str;
+11
View File
@@ -20,12 +20,17 @@
../common/features/wine.nix
../common/features/gaming
../common/features/appimage.nix
../common/features/flatpak.nix
../common/features/smb-mounts.nix
../common/features/home
];
networking.hostName = "konishi";
# Set here rather than in hardware-configuration.nix so it survives a
# nixos-generate-config regeneration; the list merges with that file's.
fileSystems."/".options = [ "noatime" ];
# NAS media share — reachable only over the wg-home VPN. Lazy automount, so it
# never blocks boot and (re)mounts on first access once the VPN is up.
custom.smbMounts = [
@@ -34,6 +39,12 @@
share = "media";
mountPoint = "/mnt/media";
}
{
# yolo VM's /home/naps62 (share defined in hosts/yolo/default.nix).
server = "10.7.10.2";
share = "home";
mountPoint = "/mnt/yolo";
}
];
# Arm Wake-on-LAN (magic packet) on the Intel igc NIC and re-apply it on every
+37 -4
View File
@@ -30,11 +30,20 @@
# this VM has a real 8G swap partition, and the host manages its own memory.
zramSwap.enable = lib.mkForce false;
# The only out-of-band way in. `vga: virtio-gl` renders through a GL context
# with no QEMU console surface, so noVNC and screendump both go dark; this
# pairs with the VM's serial0 socket to keep `qm terminal` working. Listed
# last so it wins /dev/console and gets the getty.
boot.kernelParams = [
# When the host pages this VM's memory out, KVM's async page fault parks
# the faulting task in an uninterruptible wait instead of stalling the
# whole vCPU. If the "page ready" wakeup is ever dropped, that task is
# wedged forever and SIGKILL cannot touch it — it took out maestro-web,
# nix activation generators and a dozen agent sessions, a few per day,
# until a reboot. Disabling async PF makes host page-ins stall the vCPU
# synchronously: slower under host memory pressure, but nothing hangs.
"no-kvmapf"
# The only out-of-band way in. `vga: virtio-gl` renders through a GL
# context with no QEMU console surface, so noVNC and screendump both go
# dark; this pairs with the VM's serial0 socket to keep `qm terminal`
# working. Listed last so it wins /dev/console and gets the getty.
"console=tty1"
"console=ttyS0,115200"
];
@@ -58,6 +67,30 @@
# virtio drivers; the agent itself is a separate service.
qemuGuest.enable = true;
# Exposes /home/naps62 to arrakis/konishi (custom.smbMounts on both, mounted
# at ~/yolo). Guest auth — the home dir holds SSH keys and credentials, so
# the only guard is `hosts allow` limiting clients to the wireguard subnet
# (desktops connect from wg addresses, 10.10.*).
# Clients address this VM as 10.7.10.2; keep that DHCP lease reserved.
samba = {
enable = true;
openFirewall = true;
settings = {
global = {
"map to guest" = "bad user";
"hosts allow" = "10.10.0.0/16 127.0.0.1";
"hosts deny" = "ALL";
};
home = {
path = "/home/naps62";
"read only" = "no";
"guest ok" = "yes";
"force user" = "naps62";
"force group" = "users";
};
};
};
# No physical seat: Sunshine is a user service and cannot capture until a
# graphical session exists, so a cold boot must reach one unattended. No
# hyprlock on start (unlike konishi) — nobody could type the password in.
+72
View File
@@ -0,0 +1,72 @@
{
lib,
stdenvNoCC,
fetchurl,
autoPatchelfHook,
makeWrapper,
stdenv,
}:
let
version = "0.84.2";
# Prebuilt Bun executables from the release page. Bump version + hashes from
# the SHA256SUMS asset:
# https://github.com/earendil-works/pi/releases
sources = {
x86_64-linux = {
asset = "pi-linux-x64.tar.gz";
hash = "sha256-kG++eH/SJcSsYk/n69Wx1Vpg4PXH71F5XSMVZPnuHBM=";
};
aarch64-linux = {
asset = "pi-linux-arm64.tar.gz";
hash = "sha256-0VNy2p5LTF/vn9Fb7XbX9fFyDdOf583g7GLltlrWPvE=";
};
};
source =
sources.${stdenvNoCC.hostPlatform.system}
or (throw "pi: unsupported system ${stdenvNoCC.hostPlatform.system}");
in
stdenvNoCC.mkDerivation {
pname = "pi";
inherit version;
src = fetchurl {
url = "https://github.com/earendil-works/pi/releases/download/v${version}/${source.asset}";
inherit (source) hash;
};
nativeBuildInputs = [
autoPatchelfHook
makeWrapper
];
# libgcc, for the bundled native clipboard module.
buildInputs = [ (lib.getLib stdenv.cc.cc) ];
installPhase = ''
runHook preInstall
# The binary resolves its themes, bundled node_modules and wasm relative to
# itself, so the whole tree ships together and only `pi` gets a wrapper.
mkdir -p $out/share/pi
cp -r . $out/share/pi/
chmod +x $out/share/pi/pi
# Nix owns updates here; `pi update --self` would write into the store.
makeWrapper $out/share/pi/pi $out/bin/pi \
--set-default PI_SKIP_VERSION_CHECK 1
runHook postInstall
'';
meta = {
description = "Pi coding agent CLI";
homepage = "https://pi.dev";
changelog = "https://github.com/earendil-works/pi/releases/tag/v${version}";
license = lib.licenses.mit;
sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
platforms = lib.attrNames sources;
mainProgram = "pi";
};
}
-33
View File
@@ -1,33 +0,0 @@
{ pkgs, ... }:
pkgs.stdenv.mkDerivation {
pname = "sddm-noctalia-theme";
version = "1.0";
src = pkgs.fetchFromGitHub {
owner = "mahaveergurjar";
repo = "sddm";
rev = "noctalia";
sha256 = "sha256-e/gYI6znHXxlDCOVh4p265x3kO0nQUU897hCY1yEz88=";
};
installPhase = ''
mkdir -p $out/share/sddm/themes/sddm-noctalia
cp -r * $out/share/sddm/themes/sddm-noctalia/
# Patch for Qt6 SDDM greeter compatibility
# SDDM reads QtVersion from metadata.desktop to find the greeter binary;
# default is 5 (sddm-greeter), we need 6 (sddm-greeter-qt6)
substituteInPlace $out/share/sddm/themes/sddm-noctalia/metadata.desktop \
--replace-fail "Theme-Id=noctalia" "QtVersion=6
Theme-Id=noctalia"
'';
meta = with pkgs.lib; {
description = "Noctalia-themed SDDM login theme";
homepage = "https://github.com/mahaveergurjar/sddm/tree/noctalia";
license = licenses.gpl3;
platforms = platforms.linux;
};
}