Compare commits

52 Commits

Author SHA1 Message Date
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
Miguel Palhas 3ee2f4db10 feat: run the aoe web dashboard as a service
CI / lint (push) Successful in 33s
CI / eval (push) Successful in 2m12s
Auth is off in the unit: traefik + authelia front it, and aoe rejects
--auth none on a wildcard bind unless --behind-proxy says so.
2026-08-18 09:28:32 +01:00
Miguel Palhas 886af193a2 Merge remote-tracking branch 'origin/main' into images
CI / lint (push) Successful in 28s
CI / eval (push) Successful in 1m55s
2026-08-18 09:18:29 +01:00
Miguel Palhas 4a8f8d330c fix: make clip2path safe and ssh-aware
Text paste went through `kitty @ send-text`, which has no bracketed
paste, so multi-line clipboard text ran line by line in a shell. Route
text through `kitty @ action paste_from_clipboard` instead and keep
send-text for the image path only.

Over SSH the typed path pointed at the local /tmp and the remote process
could not read it. Read the focused window's foreground processes from
`kitty @ ls`, and when one of them is an ssh client, scp the image to
that host and type the remote path. Nothing is typed into the focused
app but a path, so this still works when the target is a TUI rather than
a shell. The real ssh process is preferred over the `kitten ssh`
wrapper, whose long options the argument parser does not understand.

Also: mktemp instead of a timestamp that collides within a second,
a prune of clip files older than a day, image/png preferred over
whatever format the source advertises first, and the clipboard tools
pinned so the script does not depend on kitty's inherited PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:30:20 +01:00
Miguel Palhas 92a7273ff3 fix: let yolo inherit the common claude model
CI / lint (push) Successful in 29s
CI / eval (push) Successful in 1m53s
yolo pinned model to claude-fable-5[1m] while common sets opus[1m].
/model writes the live value into ~/.claude/settings.json, so any switch
away from fable-5 left the file drifted and mutableFiles aborted
activation on the next `nh home switch`. Dropping the override makes the
repo agree with the model actually in use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:24:00 +01:00
Miguel Palhas 05037e40d8 refactor: dissolve the agent-deck tmux block
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 1m59s
The block came from the agent-deck installer and was carried in wholesale
when the config was ported to nix. The tool is not installed on any host
and nothing else references it, and now that the file is rendered
read-only from the nix store the installer could not update its own block
anyway.

It re-declared default-terminal, history-limit and mouse at their existing
values, and escape-time at 0 — which quietly overrode the deliberate 25ms
two sections above it, along with the comment explaining that 0 breaks Alt
keys over SSH. Keeps the parts that do real work: the alternate-screen
override, extended keys, and the mouse scroll and drag-to-copy bindings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:17:45 +01:00
Miguel Palhas d417a27f2d feat: make the clipboard work across tmux, ssh and hosts
CI / lint (push) Successful in 31s
CI / eval (push) Successful in 2m1s
tmux copy bindings piped to xclip, which is X11-only and never worked
under Hyprland or over SSH, and set-clipboard was never enabled, so the
vi-mode `y` binding was dead too. Route both through OSC 52 instead.
terminal-features has to name the outer terminal explicitly: tmux skips
OSC 52 silently when the terminfo entry does not claim the capability,
which is the common case for an SSH session on xterm-256color.

Neovim only falls back to OSC 52 when it finds no clipboard tool, but
wl-clipboard is installed on every host, so over SSH it wrote the remote
clipboard. Pin the OSC 52 provider for SSH sessions only.

Moonlight and Sunshine have no clipboard channel in the protocol, so
nothing at the terminal layer can help there. Add kdeconnect on all three
hosts for that, and start its indicator from Hyprland — kdeconnectd is
DBus-activated and nothing else brings it up at login.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:14:17 +01:00
Miguel Palhas c79436d340 refactor: fix yolo config drift and restart traps
CI / lint (push) Successful in 32s
CI / eval (push) Successful in 2m7s
yolo's .claude/settings.json was a full fork of the common file, not the
narrow override its comment claimed: sandbox, defaultMode, voiceEnabled,
$schema and feedbackSurveyState were absent, and five more keys had
diverged. It now holds only yolo's overrides and merges over common via
recursiveUpdate. sandbox.enabled stays pinned false — inheriting it would
newly sandbox every Bash call on a box built for unattended agents.

mutableFiles maps a source back to a repo path for the "bring changes
upstream" hint, which only works inside the flake tree. Generated sources
would print a /nix/store path to copy onto, so add upstreamPath.

rev and rev-deploy set Restart=always with RestartSec=2, which burns
systemd's default 5-starts-per-10s budget and parks the unit in `failed`
until a manual reset-failed. StartLimitIntervalSec=0 lifts the cap.

programs.nh.flake and home.mutableFilesRepoPath become mkDefault so yolo
overrides them plainly instead of with mkForce in three places.

Also clears the outstanding nixfmt and statix findings. The yolo system
derivation hash is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:32:08 +01:00
Miguel Palhas 6a08ec3805 Merge remote-tracking branch 'origin/moonlight'
CI / lint (push) Successful in 30s
CI / eval (push) Successful in 2m5s
2026-08-17 18:22:56 +01:00
Miguel Palhas 51f6cb013f fix: drop sunshine fec_percentage to 1
At the default 20, FEC mangles large frames and moonlight draws horizontal
bands over an otherwise-fine stream, with no error on either side.
2026-08-17 18:22:52 +01:00
Miguel Palhas fbf5ce2616 fix: stream real frames from yolo instead of a black picture
Three separate faults, each producing a black Moonlight stream:

- sunshine's wlr backend takes wlr-screencopy's dmabuf path, which on this
  virtio-gpu returns empty buffers. Reproduced locally over 127.0.0.1, so it
  is not the network or the client. Switch to kms capture.
- hypridle locked the session and cut dpms, blanking the captured output with
  no console to unlock from. Disable it on this host.
- qemu picked the nvidia render node by readdir order, so virgl fell back to
  llvmpipe. Pin renderD128 via the VM's args.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:48:41 +01:00
Miguel Palhas df319322b9 fix: authenticate nix github fetches to dodge the 60/hr rate limit
CI / lint (push) Successful in 29s
CI / eval (push) Failing after 20m25s
Unauthenticated flake input fetches from github.com were hitting 429.
Token lives in /etc/nix/github-token.conf (root:root 600, out-of-band —
never in git or /nix/store) and nix.conf !includes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:03:31 +01:00
Miguel Palhas 65b95bb1fa feat: enable moonlight streaming client on konishi
CI / lint (push) Successful in 26s
CI / eval (push) Failing after 17m42s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 14:37:44 +01:00
Miguel Palhas 352700fdab fix: point nvchad-starter to git.naps.pt/naps62/nvim-config, add /bin/bash symlink
CI / lint (push) Successful in 29s
CI / eval (push) Failing after 20m59s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 14:29:39 +01:00
57 changed files with 1476 additions and 775 deletions
Generated
+98 -137
View File
@@ -23,11 +23,11 @@
},
"agent-skills": {
"locked": {
"lastModified": 1786957004,
"narHash": "sha256-lUyQGAN6Kd5onhPa+ptOiwtIaY6Y02q+nyRt9NWkdDg=",
"lastModified": 1787174393,
"narHash": "sha256-nMxPQbVaK9cdsidGP78VOxUKTAHRKHGQEzkRbKxuivY=",
"ref": "refs/heads/main",
"rev": "7cb03f879600e87281356fdc411b2690f09a9175",
"revCount": 47,
"rev": "465b20a7c64c84c645acbd94548c36c36decbfe4",
"revCount": 66,
"type": "git",
"url": "https://git.naps.pt/yolo/agent-skills.git"
},
@@ -56,11 +56,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 +71,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": 1787189302,
"narHash": "sha256-cUM75Rob89qTxJvqVRF8GUewOriqQtLiyiy5xfd1fMM=",
"owner": "sadjow",
"repo": "claude-code-nix",
"rev": "da78262708d858861afbe1f68ea65fedda4054c4",
"rev": "8fb8cbab6345583e0b68ca7dbbbad08a0a348145",
"type": "github"
},
"original": {
@@ -90,17 +90,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 +112,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": {
@@ -221,7 +221,7 @@
},
"flake-utils": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
@@ -238,24 +238,6 @@
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"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_3": {
"inputs": {
"systems": "systems_3"
},
@@ -273,7 +255,7 @@
"type": "github"
}
},
"flake-utils_4": {
"flake-utils_3": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
@@ -288,7 +270,7 @@
"type": "github"
}
},
"flake-utils_5": {
"flake-utils_4": {
"inputs": {
"systems": "systems_8"
},
@@ -308,17 +290,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 +316,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 +337,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 +358,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 +387,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 +416,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 +446,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 +494,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 +548,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 +622,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 +647,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 +672,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 +701,11 @@
]
},
"locked": {
"lastModified": 1778410714,
"narHash": "sha256-o6RzFj4nJXaPRY7EM01siuCQeT41RfwwmcmFQqwFJJg=",
"lastModified": 1786464294,
"narHash": "sha256-ZQsZ2WvBdkboCIyh8LStDPdAIARmxzn0XMNxxoOhjPE=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "85148a8e612808cf5ddb25d0b3c5840f3498a7dc",
"rev": "4ce7cd6b6128c1ac41caf23c58a30a26b327f9dd",
"type": "github"
},
"original": {
@@ -739,11 +721,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": {
@@ -800,11 +782,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1784783405,
"narHash": "sha256-4IHyyLgLBdKefkljdKod4IMn023pQiDXAWJA187cmdY=",
"lastModified": 1787111413,
"narHash": "sha256-sFosWtq21eHGJRnTc/hvf4M1obRgLEUMNm/IzllkHMA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7525d999cd850b9a488817abc89c75dc733acf17",
"rev": "afe3d8ac4395617bdcdac9f188ac8717a062e014",
"type": "github"
},
"original": {
@@ -848,11 +830,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 +846,11 @@
},
"nixpkgs_6": {
"locked": {
"lastModified": 1785090369,
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
"lastModified": 1787070829,
"narHash": "sha256-vXNVDVtvfiQuXthP0NHPFdNvvMTkGpx0UP8oddIWbNk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "624af665418d3c65d544145b4d34ad696439570e",
"rev": "0ae2bc1419c3f345984c2629e72e7a631820fa4d",
"type": "github"
},
"original": {
@@ -910,40 +892,20 @@
"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": {
"lastModified": 1782738765,
"narHash": "sha256-/jxBHJ74FYVYm8JGO3ay2J3B2/Rf3/KvroivtJFFJRM=",
"owner": "naps62",
"repo": "nvchad-starter",
"rev": "020bf7716ea1b7d233cbdaef63c8585791f896b7",
"type": "github"
"lastModified": 1786971647,
"narHash": "sha256-RsRq+BRIrpSzDUU6hF+ITwFcjtiIPsFpk220+Civ010=",
"ref": "refs/heads/main",
"rev": "4db90cb097f2358b8adb8c11f116e4201b81d0f6",
"revCount": 178,
"type": "git",
"url": "https://git.naps.pt/naps62/nvim-config.git"
},
"original": {
"owner": "naps62",
"repo": "nvchad-starter",
"type": "github"
"type": "git",
"url": "https://git.naps.pt/naps62/nvim-config.git"
}
},
"nvchad4nix": {
@@ -956,11 +918,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": {
@@ -1005,7 +967,6 @@
"hyprland": "hyprland",
"nix-index-database": "nix-index-database",
"nixpkgs": "nixpkgs_6",
"noctalia": "noctalia",
"nvchad-starter": "nvchad-starter",
"nvchad4nix": "nvchad4nix",
"rose-pine-hyprcursor": "rose-pine-hyprcursor",
@@ -1076,7 +1037,7 @@
"sem": {
"inputs": {
"flake-parts": "flake-parts_2",
"flake-utils": "flake-utils_5",
"flake-utils": "flake-utils_4",
"nixpkgs": "nixpkgs_7",
"rust-overlay": "rust-overlay"
},
@@ -1102,11 +1063,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 +1290,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 +1311,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": {
+1 -5
View File
@@ -21,7 +21,7 @@
inputs.nvchad-starter.follows = "nvchad-starter";
};
nvchad-starter = {
url = "github:naps62/nvchad-starter";
url = "git+https://git.naps.pt/naps62/nvim-config.git";
flake = false;
};
foundry = {
@@ -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";
+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
+16 -1
View File
@@ -19,7 +19,11 @@ let
let
target = "${config.home.homeDirectory}/${name}";
storePath = value.source;
originalPath = toRepoPath 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
@@ -67,6 +71,17 @@ in
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.
'';
};
};
}
);
+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;
};
}
+15 -27
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.
@@ -24,23 +20,20 @@ in
};
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,11 +41,6 @@ in
@beads/bd
'';
".claude/statusline.sh" = {
source = ./statusline.sh;
executable = 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;
};
}
+3 -7
View File
@@ -1,6 +1,5 @@
{
lib,
pkgs,
config,
...
}:
@@ -10,7 +9,6 @@
./zsh.nix
./nix.nix
./neovim
./editors.nix
./rust.nix
./elixir.nix
./nodejs.nix
@@ -30,11 +28,9 @@
};
home = {
mutableFilesRepoPath = "${config.home.homeDirectory}/projects/nixos-config";
packages = with pkgs; [
impala
];
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
# this with a plain assignment.
mutableFilesRepoPath = lib.mkDefault "${config.home.homeDirectory}/projects/nixos-config";
username = lib.mkDefault "naps62";
homeDirectory = lib.mkDefault "/home/${config.home.username}";
+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}
'';
};
};
+19 -44
View File
@@ -1,57 +1,32 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
./darkman.nix
./spicetify.nix
];
imports = [ ./darkman.nix ];
home = {
packages = with pkgs; [
# 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 +58,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;
};
+73 -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,13 +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
@@ -250,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).
@@ -273,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())
@@ -283,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"))
@@ -321,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 })
@@ -342,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
);
};
}
+99 -18
View File
@@ -1,27 +1,108 @@
{ pkgs, ... }:
let
clip2path = pkgs.writeShellScript "clip2path" ''
set -e
set -euo pipefail
if [ -n "$WAYLAND_DISPLAY" ]; then
types=$(wl-paste --list-types)
if grep -q '^image/' <<<"$types"; then
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
file="/tmp/clip_$(date +%s).''${ext}"
wl-paste --type "image/''${ext}" > "$file"
printf '%q' "$file" | kitty @ send-text --stdin
jq=${pkgs.jq}/bin/jq
scp=${pkgs.openssh}/bin/scp
wlpaste=${pkgs.wl-clipboard}/bin/wl-paste
xclip=${pkgs.xclip}/bin/xclip
find=${pkgs.findutils}/bin/find
# kitty/kitten stay unpinned: remote control has to reach the running
# instance, whose build may differ from pkgs.kitty.
"$find" /tmp -maxdepth 1 -name 'clip_*' -mtime +1 -delete 2>/dev/null || true
# Some apps advertise a lossy format before image/png.
pick_type() {
if grep -qx 'image/png' <<<"$1"; then
echo image/png
else
wl-paste --no-newline | kitty @ send-text --stdin
grep -m1 '^image/' <<<"$1" | cut -d';' -f1
fi
elif [ -n "$DISPLAY" ]; then
types=$(xclip -selection clipboard -t TARGETS -o)
if grep -q '^image/' <<<"$types"; then
ext=$(grep -m1 '^image/' <<<"$types" | cut -d/ -f2 | cut -d';' -f1)
file="/tmp/clip_$(date +%s).''${ext}"
xclip -selection clipboard -t "image/''${ext}" -o > "$file"
printf '%q' "$file" | kitty @ send-text --stdin
}
ext_of() {
local e=''${1#image/}
e=''${e%%+*}
e=''${e#x-}
tr -cd 'a-zA-Z0-9' <<<"$e"
}
# Destination of the ssh client running in the focused window, empty if
# that window is local. Prefers the real ssh process over the `kitten ssh`
# wrapper, whose own long options would confuse the parser below.
ssh_dest() {
local -a argv
mapfile -t argv < <(kitty @ ls | "$jq" -r '
[ .[] | select(.is_focused)
| .tabs[] | select(.is_focused)
| .windows[] | select(.is_focused)
| .foreground_processes[]?
| { b: ((.cmdline[0] // "") | split("/") | last), c: .cmdline }
] as $p
| ( [ $p[] | select(.b == "ssh") ] + [ $p[] | select(.b == "kitten") ] )
| (.[0].c // [])[]')
[ ''${#argv[@]} -gt 0 ] || return 0
if [ "''${argv[0]##*/}" = kitten ]; then
[ "''${argv[1]:-}" = ssh ] || return 0
argv=("''${argv[@]:2}")
else
xclip -selection clipboard -o | kitty @ send-text --stdin
argv=("''${argv[@]:1}")
fi
local valueflags=bcDEeFIiJLlmOopQRSWw a
while [ ''${#argv[@]} -gt 0 ]; do
a=''${argv[0]}
case "$a" in
--) echo "''${argv[1]:-}"; return 0 ;;
-?)
case "$valueflags" in *"''${a#-}"*) argv=("''${argv[@]:1}") ;; esac
argv=("''${argv[@]:1}")
;;
-*) argv=("''${argv[@]:1}") ;;
*) echo "$a"; return 0 ;;
esac
done
}
send_path() {
local file=$1 dest remote
dest=$(ssh_dest)
if [ -n "$dest" ]; then
remote=/tmp/''${file##*/}
if ! "$scp" -q -o BatchMode=yes -o ConnectTimeout=5 "$file" "$dest:$remote"; then
kitten notify --app-name clip2path clip2path "copy to $dest failed, nothing pasted"
return 1
fi
file=$remote
fi
printf '%q' "$file" | kitty @ send-text --stdin
}
# Text goes through the real paste action. send-text has no bracketed
# paste, so multi-line text would run line by line in a shell.
if [ -n "''${WAYLAND_DISPLAY:-}" ]; then
types=$("$wlpaste" --list-types)
if grep -q '^image/' <<<"$types"; then
type=$(pick_type "$types")
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
"$wlpaste" --type "$type" > "$file"
send_path "$file"
else
kitty @ action paste_from_clipboard
fi
elif [ -n "''${DISPLAY:-}" ]; then
types=$("$xclip" -selection clipboard -t TARGETS -o)
if grep -q '^image/' <<<"$types"; then
type=$(pick_type "$types")
file=$(mktemp --suffix=".$(ext_of "$type")" /tmp/clip_XXXXXXXX)
"$xclip" -selection clipboard -t "$type" -o > "$file"
send_path "$file"
else
kitty @ action paste_from_clipboard
fi
fi
'';
@@ -36,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;
+9
View File
@@ -12,5 +12,14 @@
programs.nvchad = {
enable = true;
# Neovim only auto-picks OSC 52 when it finds no clipboard tool, but
# wl-clipboard is on every host — so over SSH it writes the *remote*
# clipboard instead. Locally wl-copy stays: kitty refuses OSC 52 reads.
extraConfig = ''
if vim.env.SSH_TTY and vim.env.SSH_TTY ~= "" then
vim.g.clipboard = "osc52"
end
'';
};
}
+9 -2
View File
@@ -1,4 +1,9 @@
{ pkgs, inputs, ... }:
{
lib,
pkgs,
inputs,
...
}:
{
imports = [
# Prebuilt, weekly-updated nix-index database (backs `comma`).
@@ -13,7 +18,9 @@
# `flake` sets NH_FLAKE so `nh home switch` finds this repo without --flake.
programs.nh = {
enable = true;
flake = "/home/naps62/projects/nixos-config";
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
# this with a plain assignment.
flake = lib.mkDefault "/home/naps62/projects/nixos-config";
};
home.packages = with pkgs; [
-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 ];
}
-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;
}
+41 -37
View File
@@ -22,6 +22,16 @@ set -s escape-time 25
# Enable 256 colors
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"
set -ag terminal-overrides ",*256col*:Tc"
# smcup@/rmcup@ drops the alternate screen, so what tmux drew stays in the
# outer terminal's scrollback instead of vanishing when tmux exits.
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"
# -------------------------------------------------------------------
# Status bar theme
@@ -101,6 +111,37 @@ setw -g mode-keys vi
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel
# -------------------------------------------------------------------
# Clipboard (OSC 52)
# -------------------------------------------------------------------
set -g set-clipboard on
# tmux silently skips OSC 52 unless the outer terminal's terminfo claims the
# capability. kitty's does; SSH sessions usually land on a bare
# xterm-256color that does not, so declare both.
set -ag terminal-features ",xterm-kitty:clipboard"
set -ag terminal-features ",xterm-256color:clipboard"
# Let nvim's OSC 52 provider reach the outer terminal through tmux.
set -g allow-passthrough on
# Drag-to-copy. No external command: set-clipboard routes this through
# OSC 52, which works on Wayland and over SSH. xclip did neither.
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel
# -------------------------------------------------------------------
# Mouse scrolling
# -------------------------------------------------------------------
# Scrolling up enters copy-mode, unless the pane's application wants the
# mouse itself or we are already in copy-mode.
bind-key -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e'"
bind-key -T copy-mode-vi WheelUpPane send-keys -X scroll-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X scroll-down
bind-key -T copy-mode WheelUpPane send-keys -X scroll-up
bind-key -T copy-mode WheelDownPane send-keys -X scroll-down
# tmux-resurrect settings
set -g @resurrect-capture-pane-contents 'on'
@@ -109,40 +150,3 @@ set -g @resurrect-strategy-nvim 'session'
# tmux-continuum settings (auto-save and auto-restore)
set -g @continuum-restore 'on'
set -g @continuum-save-interval '10'
# agent-deck configuration
# agent-deck-tmux-config-version: 3
# Added by agent-deck installer - 2026-03-18
# https://github.com/asheshgoplani/agent-deck
# Terminal with true color support
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm*:Tc:smcup@:rmcup@"
set -ag terminal-overrides ",*256col*:Tc"
# Performance
set -sg escape-time 0
set -g history-limit 50000
# Extended keys: forward Shift+Enter and other modified keys to apps (tmux 3.2+)
set -s extended-keys on
set -as terminal-features 'tmux-256color:extkeys'
# Mouse support (scroll + drag-to-copy)
set -g mouse on
# Auto-enter copy-mode when scrolling up (critical for WSL compatibility)
# This handles: 1) apps with mouse support, 2) already in copy-mode, 3) normal pane
bind-key -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e'"
# Scroll bindings in copy-mode (both vi and emacs modes)
bind-key -T copy-mode-vi WheelUpPane send-keys -X scroll-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X scroll-down
bind-key -T copy-mode WheelUpPane send-keys -X scroll-up
bind-key -T copy-mode WheelDownPane send-keys -X scroll-down
# Clipboard integration (drag-to-copy)
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "xclip -in -selection clipboard"
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "xclip -in -selection clipboard"
# End agent-deck configuration
+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
];
}
+7 -8
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
@@ -29,12 +33,15 @@
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 = {
enable = true;
nvidia = true;
retroarch = true;
moonlight = true;
};
# 4K@1x monitors render the Electron AI apps tiny; scale their UI up. Tune to taste.
@@ -46,7 +53,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
@@ -109,12 +115,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;
}
+3 -5
View File
@@ -9,7 +9,7 @@ compaction_reminder_percent = 75
default_agent = "claude"
max_concurrent_workers = 5
node_path = ""
offer_structured_in_new_session = false
offer_structured_in_new_session = true
rate_limit_auto_resume = false
replay_events = 0
restrict_agents = false
@@ -79,7 +79,7 @@ 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
show_tips = false
smart_rename = true
smart_rename_agent = ""
snooze_duration_minutes = 30
@@ -90,11 +90,9 @@ unread_indicator = true
yolo_mode_default = true
[session.agent_detect_as]
kimiclaude = "claude"
synclaude = "claude"
[session.custom_agents]
kimiclaude = "kimiclaude"
synclaude = "synclaude"
[skills]
@@ -112,7 +110,7 @@ enabled = false
[theme]
color_mode = "truecolor"
idle_decay_minutes = 0
name = ""
name = "zinc"
[tmux]
clipboard = "auto"
+3 -7
View File
@@ -22,7 +22,6 @@
],
"defaultMode": "auto"
},
"model": "claude-fable-5[1m]",
"hooks": {
"PostToolUse": [
{
@@ -200,10 +199,6 @@
"worktree": {
"baseRef": "fresh"
},
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
},
"enabledPlugins": {
"building@subvisual": true,
"rust-analyzer-lsp@claude-plugins-official": false,
@@ -229,7 +224,6 @@
}
}
},
"tui": "fullscreen",
"skipDangerousModePermissionPrompt": true,
"theme": "dark",
"editorMode": "vim",
@@ -237,7 +231,6 @@
"autoCompactEnabled": true,
"remoteControlAtStartup": false,
"inputNeededNotifEnabled": true,
"agentPushNotifEnabled": true,
"mcpServers": {
"playwright": {
"args": [
@@ -247,5 +240,8 @@
"command": "npx",
"type": "stdio"
}
},
"sandbox": {
"enabled": false
}
}
+67 -15
View File
@@ -3,6 +3,26 @@
pkgs,
...
}:
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
)
);
# Same merge, same reason: ./opencode.json holds only yolo's overrides so
# agents, commands and skills keep coming from common.
mergedOpencodeConfig = (pkgs.formats.json { }).generate "opencode.json" (
lib.recursiveUpdate (lib.importJSON ../common/programs/opencode/opencode.json) (
lib.importJSON ./opencode.json
)
);
in
{
imports = [
../common/programs/default.nix
@@ -17,27 +37,59 @@
./ssh.nix
];
# Host-local, like the aoe config below: 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;
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
home.packages = [ pkgs.agent-browser ];
# 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";
};
# 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.
home.mutableFilesRepoPath = lib.mkForce "/home/naps62/tea/nixos-config";
home = {
# Headless browser driver the agent tooling shells out to. Was a global npm
# install on the ubuntu box.
packages = [ pkgs.agent-browser ];
# Host-local, not shared: this sets yolo_mode_default = true, which starts aoe
# sessions with permission checks skipped. Only correct on this box.
home.mutableFiles.".config/agent-of-empires/config.toml".source = ./aoe-config.toml;
# 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";
# Likewise host-local: carries skipDangerousModePermissionPrompt and the rev
# hook paths, neither of which belong on a workstation.
home.mutableFiles.".claude/settings.json".source = lib.mkForce ./claude-settings.json;
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;
programs.agentSkills.machine = "yolo";
programs.nh.flake = lib.mkForce "/home/naps62/tea/nixos-config";
# 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;
};
};
};
# 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
# flat frame and Moonlight goes black, with no console to unlock from.
services.hypridle.enable = lib.mkForce false;
# Blur and shadow cost a fullscreen pass per frame, and every frame here is
# also x264-encoded for the stream — on a virtio-gpu with no VirGL, in software.
+6 -3
View File
@@ -1,11 +1,14 @@
_:
{
_: {
# Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland).
#
# 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"
}
}
+27 -51
View File
@@ -15,6 +15,8 @@ let
toolPath = "%h/.local/bin:%h/.nix-profile/bin:/etc/profiles/per-user/naps62/bin:/run/current-system/sw/bin";
sem = inputs.sem.packages.${pkgs.system}.default;
aoe = inputs.agent-of-empires.packages.${pkgs.system}.aoe-with-web;
in
{
home.packages = [
@@ -27,6 +29,10 @@ in
Unit = {
Description = "rev always-on local code review server";
After = [ "network.target" ];
# MUST stay 0: at RestartSec=2 a fast-crashing rev 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";
@@ -51,74 +57,44 @@ in
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}" ];
# Unit files are home-manager symlinks; deploy.sh must not rewrite them.
Environment = [ "PATH=${toolPath}" "REV_SKIP_UNIT_INSTALL=1" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
};
hourlog = {
aoe-web = {
Unit = {
Description = "Start the Friday hour log in a tmux session";
Documentation = [ "https://git.naps.pt/yolo/agent-skills" ];
ConditionPathIsDirectory = "%h/tea/agent-skills";
Description = "aoe serve Agent of Empires web dashboard";
After = [ "network.target" ];
# Same restart-budget trap as `rev` above.
StartLimitIntervalSec = 0;
};
Service = {
Type = "oneshot";
ExecStart = "%h/tea/agent-skills/bin/hourlog-session.sh";
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}" ];
# 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}" ];
KillMode = "process";
Restart = "always";
RestartSec = 2;
};
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";
+7
View File
@@ -17,6 +17,7 @@
../common/features/docker.nix
../common/features/appimage.nix
../common/features/fonts.nix
../common/features/kdeconnect.nix
../common/features/nix-ld.nix
../common/features/bluetooth.nix
../common/features/ledger.nix
@@ -34,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 = "/home/naps62/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;
@@ -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
+10
View File
@@ -0,0 +1,10 @@
{ pkgs, ... }:
{
# Clipboard (and file) sync between hosts over the LAN. Moonlight/Sunshine
# carry no clipboard channel at all, so this is the only path between the
# streaming client and its host.
programs.kdeconnect = {
enable = true;
package = pkgs.kdePackages.kdeconnect-kde;
};
}
+20 -3
View File
@@ -11,11 +11,16 @@ _:
# 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;
openFirewall = true;
capSysAdmin = false; # wlr-screencopy needs no CAP_SYS_ADMIN
capSysAdmin = true; # kms capture reads the scanout framebuffer via DRM
settings = {
# MUST be set. Auto-probe tries portalgrab first, and
@@ -23,11 +28,17 @@ _:
# probe then hangs forever instead of falling back, so sunshine never
# binds its ports and the unit sits "active" doing nothing.
#
# "wlr", not "wlgrab": wlgrab is the name this backend logs under, but it
# "kms", not "wlr": the wlr backend takes wlr-screencopy's dmabuf path,
# which on this virtio-gpu returns empty buffers — sunshine encodes a flat
# frame (~50-byte P-frames) and every client shows black, with no error on
# either side. grim still works because it uses the shm path, and sunshine
# exposes no way to force shm.
#
# "wlr", not "wlgrab": wlgrab is the name that backend logs under, but it
# is not accepted as a value — sunshine rejects it with "Unable to
# initialize capture method", then still starts and binds its ports with
# no encoder, so the failure only shows up as a 503 in moonlight.
capture = "wlr";
capture = "kms";
# The web UI is only reachable over the network here — there is no local
# browser — and sunshine CSRF-rejects any origin but localhost unless it
@@ -37,6 +48,12 @@ _:
# and never strips brackets or quotes, so "[...]" makes every entry fail
# its starts_with("https://") check.
csrf_allowed_origins = "https://10.7.250.1:47990,https://10.7.10.2:47990";
# At the default 20, sunshine's FEC mangles large frames and moonlight
# draws horizontal bands over an otherwise-fine stream — no error on
# either side, and the network shows 0% loss. konishi carries the same
# workaround in its hand-written ~/.config/sunshine/sunshine.conf.
fec_percentage = 1;
};
};
}
+6
View File
@@ -33,6 +33,12 @@
services.dbus.packages = with pkgs; [ gcr ];
# NixOS provides /bin/sh but not /bin/bash. Some third-party scripts
# hardcode #!/bin/bash, so provide it too.
system.activationScripts.binbash = ''
ln -sf ${pkgs.bash}/bin/bash /bin/bash
'';
# Compressed RAM swap — better behaviour under memory pressure.
zramSwap.enable = true;
+11 -1
View File
@@ -23,6 +23,14 @@
# GC is handled by `nh clean` below (keep-N / keep-since semantics).
# The two are mutually exclusive — nh asserts if nix.gc.automatic is on.
gc.automatic = false;
# Unauthenticated GitHub API calls (flake input fetches) are capped at
# 60/hr and 429 quickly. The token itself must not land in /nix/store
# (world-readable), so it lives in a plain file outside Nix's management —
# !include reads it at nix.conf parse time instead of embedding it.
extraOptions = ''
!include /etc/nix/github-token.conf
'';
};
# nh: ergonomic nixos-rebuild wrapper. Auto-detects the target from the
@@ -30,7 +38,9 @@
# no host arg. Runs as root, so its `clean` prunes system + user profiles.
programs.nh = {
enable = true;
flake = "/home/naps62/projects/nixos-config";
# mkDefault: hosts whose clone lives elsewhere (yolo, under ~/tea) override
# this with a plain assignment.
flake = lib.mkDefault "/home/naps62/projects/nixos-config";
clean = {
enable = true;
extraArgs = "--keep 10 --keep-since 7d";
+7
View File
@@ -13,6 +13,7 @@
../common/features/pipewire.nix
../common/features/docker.nix
../common/features/fonts.nix
../common/features/kdeconnect.nix
../common/features/nix-ld.nix
../common/features/bluetooth.nix
../common/features/ledger.nix
@@ -33,6 +34,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 = "/home/naps62/yolo";
}
];
# Arm Wake-on-LAN (magic packet) on the Intel igc NIC and re-apply it on every
+32 -36
View File
@@ -17,7 +17,8 @@ qm create 132 --name yolo \
--vga virtio-gl \
--serial0 socket \
--agent 1 \
--onboot 1
--onboot 1 \
--args '-display egl-headless,gl=core,rendernode=/dev/dri/renderD128'
```
After creating the disk, drop its ZFS reservation — the `data` pool has no
@@ -27,12 +28,15 @@ After creating the disk, drop its ZFS reservation — the `data` pool has no
zfs set refreservation=none data/vm-132-disk-1
```
## The three that matter
## The four that matter
**`--vga virtio-gl`.** Not for speed — the host's virglrenderer falls back to
llvmpipe either way. It is what lets mesa allocate GBM buffers. With plain
`virtio` every capture fails with `Failed to create GBM buffer`, and sunshine
streams a black picture while reporting no error at all.
**`--vga virtio-gl`.** Lets mesa allocate GBM buffers. With plain `virtio`
every capture fails with `Failed to create GBM buffer`, and sunshine streams a
black picture while reporting no error at all.
**`--args '-display egl-headless,...,rendernode=/dev/dri/renderD128'`.** Pins
host-side rendering to the UHD 770. Without it the guest renders on llvmpipe —
see below.
**`--serial0 socket`.** `virtio-gl` renders through a GL context with no QEMU
console surface, so the Proxmox noVNC console and `qm screendump` both go dark.
@@ -42,44 +46,36 @@ This plus `boot.kernelParams = [ "console=ttyS0,115200" ]` keeps
**`--bios ovmf`.** `hosts/common/features/user.nix` uses systemd-boot, which
needs UEFI. SeaBIOS gives an unbootable disk.
## Known limit: no GPU acceleration
## Why the render node has to be pinned
The guest reports `virgl (LLVMPIPE)`, so compositing and capture readback are
both on CPU. Hyprland delivers ~2fps at 2560x1440, sunshine asks for 60, and
moonlight disconnects. Capture and encoding are otherwise fine — x264 manages
1440p at 3x realtime here.
pve 8.4.1 hardcodes `-display egl-headless,gl=core` in
`/usr/share/perl5/PVE/QemuServer.pm` with no `rendernode=`. qemu then picks a
node itself by scanning `/dev/dri` in `readdir` order and taking the first
`renderD*` that opens. Here that lands on renderD129 — the nvidia card, which
is reserved for LXCs. mesa cannot drive it, so virglrenderer falls back to
software and the guest reports `virgl (LLVMPIPE)`: Hyprland manages ~2fps at
2560x1440 while sunshine asks for 60, and moonlight disconnects.
The host can do better: `iris_dri.so` is present and `i915` is loaded on
renderD128 (the UHD 770; renderD129 is the nvidia card, reserved for LXCs and
not available to VMs). Two things stop it being used:
- pve 8.4.1 hardcodes `-display egl-headless,gl=core` in
`/usr/share/perl5/PVE/QemuServer.pm` with no `rendernode=`, so qemu picks the
EGL device itself.
- `/usr/share/glvnd/egl_vendor.d/` lists `10_nvidia.json` ahead of
`50_mesa.json`, and lower wins — so EGL initialises against nvidia and falls
back to llvmpipe.
To test the diagnosis without changing anything, start the VM with the mesa
vendor forced:
The `--args` line overrides this. PVE appends `args` after its own `-display`,
and qemu's last `-display` wins, so rendering is pinned to renderD128 (the UHD
770, `i915`) without touching the packaged perl — which any pve-manager upgrade
would revert. Confirm from the guest:
```
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json qm start 132
grep "Renderer:" /run/user/1000/hypr/*/hyprland.log
```
If the guest then reports `iris` rather than `LLVMPIPE`, persist it with a
drop-in rather than by editing the packaged perl — drop-ins survive upgrades,
and the nvidia LXCs are unaffected because they start via pve-container:
`virgl (Mesa Intel(R) Graphics (RPL-S))` is correct; `LLVMPIPE` means the
override is not taking. On the host, `ls -l /proc/$(cat
/var/run/qemu-server/132.pid)/fd | grep dri` shows which node qemu actually
holds.
```
systemctl edit pvedaemon
[Service]
Environment=__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
```
Forcing the EGL vendor instead (`__EGL_VENDOR_LIBRARY_FILENAMES=.../50_mesa.json`)
does not help and was tried: the vendor was never the problem, the node was.
Last resort only, if EGL still picks wrong: append
`,rendernode=/dev/dri/renderD128` to the QemuServer.pm line above. That edits a
packaged file and is reverted by any pve-manager upgrade.
Encoding stays on x264. virgl accelerates GL only — the guest sees a
virtio-gpu, not the Intel device, so there is no VAAPI/QuickSync encode path.
Not a bottleneck: x264 manages 1440p at 3x realtime here.
## Network
+58 -32
View File
@@ -14,6 +14,7 @@
../common/features/remote-desktop.nix
../common/features/docker.nix
../common/features/fonts.nix
../common/features/kdeconnect.nix
../common/features/nix-ld.nix
../common/features/appimage.nix
../common/features/home
@@ -24,11 +25,6 @@
# screen and reports no error.
networking.hostName = "yolo";
# Lets the Proxmox host drive clean shutdowns/reboots and report the guest's
# IP. The qemu-guest profile in hardware-configuration.nix only sets up the
# virtio drivers; the agent itself is a separate service.
services.qemuGuest.enable = true;
# Hung shutdown waiting on systemd-zram-setup@zram0: deactivating zram swap
# has to fault every stored page back into RAM first. Redundant here anyway —
# this VM has a real 8G swap partition, and the host manages its own memory.
@@ -54,35 +50,65 @@
environment.systemPackages = [ pkgs.git ];
# This clone lives under ~/tea, not the ~/projects path global/nix.nix assumes.
programs.nh.flake = lib.mkForce "/home/naps62/tea/nixos-config";
programs.nh.flake = "/home/naps62/tea/nixos-config";
# 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.
services.displayManager = {
autoLogin = {
services = {
# Lets the Proxmox host drive clean shutdowns/reboots and report the guest's
# IP. The qemu-guest profile in hardware-configuration.nix only sets up the
# 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;
user = "naps62";
};
defaultSession = "hyprland";
};
# A Proxmox guest has no emulated sound card, so PipeWire starts with no sink
# and Sunshine has nothing to capture — the stream is silent. This sink is the
# only one, so it wins the default and Sunshine records its monitor.
services.pipewire.extraConfig.pipewire."10-null-sink" = {
"context.objects" = [
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "sunshine-sink";
"node.description" = "Sunshine";
"media.class" = "Audio/Sink";
"audio.position" = "FL,FR";
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.
displayManager = {
autoLogin = {
enable = true;
user = "naps62";
};
defaultSession = "hyprland";
};
# A Proxmox guest has no emulated sound card, so PipeWire starts with no
# sink and Sunshine has nothing to capture — the stream is silent. This sink
# is the only one, so it wins the default and Sunshine records its monitor.
pipewire.extraConfig.pipewire."10-null-sink" = {
"context.objects" = [
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "sunshine-sink";
"node.description" = "Sunshine";
"media.class" = "Audio/Sink";
"audio.position" = "FL,FR";
};
}
];
};
};
}
+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;
};
}