Two machines. Opposite palettes. One idea.

An Arch/Hyprland desktop that is built and running, and a NixOS/niri laptop that is specced with early phases landing. This deck adopts each machine's real palette as you move through it, the colors are transcribed from the running hosts.

00 · cold open

Two machines. Opposite palettes. One idea.

One is dark, neon, and glowing. One is light, quiet, and Nordic. Two machines, two vibes.

to move through swipe to move through

desktop
archbox
laptop
nixbox
os
Arch + Hyprland
os
NixOS + niri
gpu
RTX 4070 Ti
gpu
Intel UHD 620
state
built · running
state
specced · landing

both machines are real; screenshots pending

01 · thesis

A rice is a build, not a pile of symlinks.

Both machines have exactly one source of truth for color, and everything downstream is generated from it. They only differ on which direction the arrow points.

The split isn't only cosmetic. It's also about what you can safely change, what you can roll back, and whether "the config" is a thing you edit or a thing you produce.

In other words, both run (very) different distros. The name says it all: the archbox runs an Arch-based distro, while the nixbox is on NixOS.

live config ──▶ repo
repo ◀── live config
archbox · dotstack
sync up
nixbox · flake
generate down
source of truth · one file each
-- archbox
~/.config/hypr/colors.lua

# nixbox
modules/palette.nix
02 · two palettes each

Each machine runs two palettes.

Same instinct, solved twice: one palette can't do every job. Full neon everywhere is fine until you have to read it all day.

Archbox splits by function. Vivid Cyberdream for chrome you glance at: borders, lock screen, bar accents. Softer TokyoNight Storm for anything you read for an hour: terminal, Neovim, the Qt apps whose lists you scan.

Nixbox splits by time. Snow Storm by day, Polar Night after dusk, both prebuilt as variants and swapped without a rebuild.

archbox · split by function
glance / read
nixbox · split by time
day / night
cyberdreamglance · #16181a
cyanprimary · #5ef1ff
tn stormread · #24283b
bluelink · #7aa2f7

The two archbox palettes agree on the color doing most of the work: the Cyberdream cyan and TokyoNight's teal sit 4 degrees apart in hue. Only the pink and green were orphans, and both were replaced with the terminal's own values. The bases stay deliberately mismatched, so the terminal reads as lifted and floating rather than flush with the wallpaper.

03 · the machine

archbox

The one that's finished, at least as much as anything ever is. Built for throughput and glow, on hardware that doesn't need to apologize (or hang) for a compositor effect.

distroArch-based fork
kernellinux-zen
compositorHyprland (wayland)
gpuNVIDIA RTX 4070 Ti
bar / hudeww + hyprglaze
palettecolors.lua → 4 consumers
statebuilt · daily driver
act i · desktop
cyberdream mock 1 2 3 4 ▂▅▃▇▅ 18ms 4070 ti · 41°C ♪ Boards of Canada vol 40 21:14
ghostty — fish
check-colors.sh
reading colors.lua … 8 values
shaders/cyberdream-drift.frag 8 ok
eww/eww.scss 8 ok
hyprglaze-themes.json 8 ok
no drift across 4 consumers
hyprctl monitors | rg refresh
refreshRate: 164.96 Hz
scale: 1.00
qutebrowser
wiki.archlinux.org/title/Hyprland
Hyprland
A dynamic tiling Wayland compositor
based on wlroots that doesn't sacrifice
on its looks.
Contents
1 Installation
2 Configuration
3 Variables
4 Window rules
❯ col
colors.lua — palette source of truth
check-colors.sh — drift guard
the look · not a screenshot
05 · reading surfaces

You've been reading for a while now.

The deck just changed palette. Everything from here is TokyoNight Storm, the palette the desktop uses for surfaces you read.

Chrome is glanced at; a terminal is stared into. Full-saturation neon works for a border you register in peripheral vision, but wrong for a buffer you scan for an hour. The transition you just watched is the argument; your eyes should be telling you the difference.

above · chromecyberdream #5ef1ff
here · readingtn storm #7aa2f7
hue delta
differs insaturation — the point
qt6ct/colors/tokyonight-storm.conf
# reading surfaces only
bg        #24283b
bg_dark   #1f2335
fg        #c0caf5
comment   #565f89
blue      #7aa2f7  -- link
teal      #2ac3de  -- selection

values transcribed from colors.lua · M.tn

06 · dotstack

The desktop's real differentiator isn't the wallpaper.

It's dotstack a homegrown system that takes components, package lists, harnesses, sync scripts, and skills that keep a snapshot of a live workstation.

The arrow points up. You change the running machine, and the sync pulls that change into the repo as a described component rather than a captured blob. Restore runs the same components in the other direction.

live config~/.config, the running machine
syncupdate-all-with-dotstack-sync
repocomponents · markers · packages
dotstack/
components/   per-app install + restore units
config/       tracked dotfiles
lib/          shared shell harness
markers/      what is managed, and by whom
packages/     explicit package sets
restore-all   the other direction

Four files carry the palette's hex values and cannot read the Lua table — a GLSL shader, a SCSS file, a daemon's JSON, and the Lua itself. check-colors.sh greps all four and exits non-zero if they've drifted. Duplication you can't remove is duplication you test.

07 · the orchestration log

Which agent to trust is an empirical question.

Several models and harnesses maintain these machines. Deciding which one gets which job might start on vibes, but should build evidence that converges on what's ideal in which harness for what situation. Enter a recent project: the Orchestration Logs.

Every orchestration session also creates a dated note with re-runnable evidence: a command, a grep, a file:line. Opinions don't survive distillation. The entries are sorted by what actually failed, which is the field that earns the log its keep:

harnessbuffered output read as a stall
providera 429 read as incompetence
orchestratormy prompt was the defect
modelonly what's left

The tally is the punchline. Across 43 logged entries, harness and provider problems outnumber model problems roughly two to one — most "the AI failed" moments were plumbing.

act i · desktop
08 · war story

Xid 175: black screen diagnosis.

A tale of trying to debug constant black screens, perseverance, and a little (a lot of) help from my friends (/agents). Provided as an example of a day in the Linux driver life.

First read: a render submission fault. Plausible, matched the vibe of the crash, and would have sent me into driver-version roulette and a GPU swap. It was wrong on three independent axes.
Axis one — the workload. Every single crash was during hardware video decode. Not gaming, not compute. A render path that only fails on decode isn't a render path.
Axis two — the Xid string itself. It was GSP_RM_CONTROL. That's the GPU system processor's resource-manager control channel, not a graphics submission.
Axis three — the trace. The stack was a DMA teardown, not a submission. The failure is on the way out of an operation, not on the way in.
Three independent signals beat one confident hypothesis. All three pointed at decode-path resource teardown. Cheap test before expensive test: change the decode path, not the hardware.
reasoning, not conclusion
cpui7-8565U
gpuIntel UHD 620
esp free913 MiB / 1 GiB
ram31 GiB
compositorniri (scrollable)

nixbox · gen 36 · 2026-08-17

09 · constraints as design driver

The hardware defines the aesthetic.

No video wallpaper. No heavy blur. UHD 620 will render a compositor beautifully and will not render a compositor showing off.

The boot partition used to be the sharper constraint: 12 MiB free on a shared 100 MiB ESP, one existing initrd eating ~42 MiB, and no room for a second. That single number ended up being a whole argument against using Plymouth for login. We removed the old Windows install, repartitioned, and /boot became a dedicated 1 GiB partition with 913 MiB free.

The constraint evaporated but Plymouth still isn't on the roadmap, classic case of an old implementation staying in even once the reasons change. But it could go in now.

Constraints don't produce a compromised design. They produce a specific one: flat surfaces, real contrast, geometry instead of effects.

#5E81AC does about 90% of the accent work. Aurora colors appear only when they mean something — an error, a warning, a live state. A palette where every color is available everywhere is a palette that communicates nothing.

nixbox · gen 36 · 2026-08-17

10 · the snow storm palette

One accent, held to it.

baseground · #ECEFF4
surfaceraised · #E5E9F0
borderrules · #D8DEE9
textprimary · #2E3440
accentfills · #5E81AC
accent inktext <18px · #43648E
fillsecondary · #88C0D0
rederror only · #BF616A

The two accents are disciplined. Nord's #5E81AC is valid as a fill, a ring, or large display type, but would fail contrast as small text on a Snow Storm ground. So small text uses a derived ink value, and a helper called pickInk makes that choice rather than each generator guessing.

modules/palette.nix
frost = {
  accent = "#5E81AC";
  hl     = "#81A1C1";
  fill   = "#88C0D0";
};

nixbox · gen 36 · 2026-08-17

11 · the pipeline

Change one value, the whole desktop repaints.

palette.nixone source of truth
generatorsper-app theme formats
home-managerdeclared, not copied
switcha numbered generation

Every generated theme traces back to the palette; a generator that hardcodes a hex value is a bug. Contrast is not a review step. Programmatic WCAG tests enforce 4.5:1 for normal text and 3:1 for large text and non-text UI across every required pair.

Because each activation is a generation, every repaint is rollback-able. The worst outcome of a bad color is a boot menu entry.

act ii · laptop
12 · the astronomy engine

An all-sky chart that is verifiable.

NE WS 60°30°
un-blue-instrument-v1zenith center · horizon rim

Zenith at the center, horizon at the circumference, North up, East left because you are looking up, not down at a map.

Eight fixture cities pin the math. Sydney catches hemisphere errors: Crux must be above the horizon and Polaris below. Quito puts both poles near the rim. Get a sign wrong anywhere and a fixture fails.

Identical input produces byte-identical output. Because even lock screens deserve to be spectacular.

nixbox · gen 36 · 2026-08-17

solar wallpaper · 14:20 UTC frame

nixbox · gen 36 · 2026-08-17

13 · generative wallpaper

The geometry is fixed for the day. The light isn't.

The civil date is the seed to produce one stable mountain geometry. Sun position and ambient light then progress through UTC-keyed frames, rendered from the same ephemeris the star chart uses.

The wallpaper is not a slideshow or animation; it's a function of the date and the clock, and it selects the correct frame rather than fading between images. Re-running the selector when nothing has changed is a no-op.

station time
14:32
uptime3h 12m
generation36 (current)
wx · open-meteo
now3°C · light snow
temp °Cnext 24h
pwr · net
battery81% · 3h12m
cpu12%

mockdestination state · phase 5 pending

14 · station instruments

Every surface reads the same cache.

Waybar and the quickshell HUD are consumers, not data sources. Weather, VPN state, astronomy, and theme each publish one schema-checked cache; every instrument reads it.

The rule that keeps it honest: no duplicate data daemons. If the bar and the HUD disagree about the temperature, that is a bug in one cache, not a race between two pollers. Stale and offline are explicit states with their own rendering, not a blank field.

specced · quiet for 23 days
15 · progress · live

Where it actually is.

00 repo migration, contracts, safety plane done
01 flake/HM parity and palette done
02 safe core desktop done
03 astronomy, artwork, solar wallpaper done
04 terminal, applications, VPN consumer done
05 shared instruments, signature features active
06 provider-neutral VPN pending
07 ReGreet rollout, human validation pending

5 of 8 phases done. Phase state is a declared human judgment. "Acceptance criteria met" is not something a script gets to infer. Everything below it is automatic.

2026-08-17 switch · 36 niri workspaces + instrument row finally on screen
2026-08-17 test · 36 astro status split: 1.9 s compute -> 6 ms cache read
2026-08-14 recover · 22 gens 4+ had no ESP entry; boot staging amended in
blocked · Kanata — implemented but disabled; needs a timed physical keyboard trial
blocked · remote backup — Restic/B2 architecture decided, endpoint not yet named
blocked · VPN adapter — provider adapter is build-only; no install, auth, or live test

specced · quiet for 23 days · source rice-progress.json seed data · not yet host-bound

16 · the deadman

How do you let an agent edit the machine it is running on?

That is the actual question. An agent working on deep internals - bootloader, kernel, display stack - can sever the connection it would need to undo its own change. Ask it to fix the thing, and the failure mode isn't a bad diff. It's a laptop that no longer boots and no longer answers.

Code review doesn't help: the change is correct right up until it isn't, and by then nothing is listening. So the machine gets a deadman switch. A guard is armed with a ten-minute timer before the change, and if nobody disarms it, the host restores itself to a pinned known-good closure. That covers both a hung switch and a dead network equally, because both look identical from outside: silence.

Failure and unknown are the same outcome: don't disarm, don't retry in place, let the timer fire. A machine that recovered itself is a correct result, not an incident.

non-boot change · the whole ritual
tx=$(sudo station-guard arm 10)
sudo station-deploy test "$closure"
station-health --expect test
# feature probes + screenshot review
sudo station-deploy switch "$closure"
station-health --expect switch
sudo station-guard disarm "$tx"
invariantswitch promotes the same closure that passed test
on failuredo not disarm
recoveryprofile repaired, then runtime-only activation
gcnever — known-good pins the closure, not the generation link
17 · the credential audit

A passing dry run counts as a failure.

The whole machine is a git repository. Every package, service, keybind and color is declared in Nix, so a rebuild reads the repo and produces a byte-identical system. nixbox is compiled, not accumulated.

That is the reproducibility win, and it hands you the security problem attached to it: the repo can now rewrite the machine. So nixbox pulls with repository-scoped, read-only deploy keys. It can read its own configuration and cannot write back. Proving they are read-only is where the trust-boundary thinking shows up.

Push a unique ref with --dry-run through each alias and require an explicit permission rejection. If git answers Everything up-to-date, the probe proved nothing — it never reached the permission check. A successful fetch proves nothing either.

The default GitHub credential path is configured to fail closed before any key is installed, and a fresh clone that misses the local override is supposed to fail. Never retry through the default host or a forwarded agent.

read-only proof · both aliases
$ git push --dry-run parent HEAD:probe-x
remote: Permission denied to deploy key
✓ rejected — this is the pass

$ git push --dry-run child HEAD:probe-x
Everything up-to-date
✗ FAIL — proves nothing, never
  reached the permission check

The same instinct as a protocol audit: an assertion that passes for the wrong reason is worse than no assertion, because it retires the question.

18 · close

Shipped, and specced.

One machine is finished and drives daily. One is a spec with more and more phases landing. This page says so on every slide rather than in a footnote.

The thesis holds in both directions. Whether the arrow points from the machine to the repo or from the repo to the machine, there is exactly one place a color is defined, and everything else is generated, tested, and reversible.

← back to wschwab.xyz
archbox · shipped
daily driver
nixbox · specced
phase 5 of 8
palette source
colors.lua
palette source
palette.nix
direction
live → repo
direction
repo → live
recovery
restore-all
recovery
deadman + known-good

specced · quiet for 23 days · this page rebuilds daily and recomputes its own status

wschwab.xyz · 00 / 18 · · esc → site