A theme switch keeps the stream's resolution, and the console wears the whole theme #437

Merged
enricobuehler merged 1 commits from worktree-omarchy-theme-live into main 2026-08-28 22:00:15 +00:00
1 Commits
Author SHA1 Message Date
enricobuehler 55b1dc848d A theme switch keeps the stream's resolution, and the console wears the whole theme
ci / web (pull_request) Successful in 1m19s
ci / rust-arm64 (pull_request) Successful in 2m28s
ci / bun-nix (pull_request) Successful in 39s
ci / docs-site (pull_request) Successful in 59s
ci / docs-drift (pull_request) Successful in 28s
android / android (pull_request) Failing after 7m39s
nix / flake (pull_request) Successful in 8m58s
ci / rust (pull_request) Successful in 24m56s
Two findings from an Omarchy field session, one root cause each.

## The output resolution reset on every theme change

`omarchy-theme-set` ends in `omarchy-restart-hyprctl`, which is `hyprctl reload`
— and a reload drops EVERY runtime `hyprctl keyword`, our monitor rule for the
streamed head included. This crate already documented that fact (it is what
`restore_heads` relies on to re-light an `exclusive` session's monitors); nothing
put the rule back. So the head silently fell to its default mode mid-stream.
Resizing the client window appeared to fix it only because a resize on Linux
re-creates the output, which runs `set_monitor_rule` again.

The Hyprland backend now subscribes to the compositor's event socket and
re-applies the rule on `configreloaded`. Any reload gets it — the operator's own
`hyprctl reload`, `omarchy-refresh-config`, a theme switch — not just the one
that was reported. The watcher is a socket read, not a poll: an idle session
costs nothing, and its guard shuts the socket down on teardown so the thread
cannot outlive the output (sessions are minted on every mid-stream resize).

The mode only. A reload also undoes `topology: exclusive`'s head disables, but
re-disabling them from this watcher risks a permanently dark desk — teardown's
own `restore_heads` runs a reload to re-light them, and nothing orders the two
today. Noted in the code where the fix belongs.

## The console did not follow the theme, and only half wore it

Two separate causes:

* `useUiConfig` was `staleTime: Infinity`. The ports on that payload genuinely
  cannot change without a restart, but the theme can, so the console sat in the
  old palette until someone reloaded it. Now polled at 2 s (and refetched on
  focus). Polled and not pushed because the console's SSE stream is a proxy of
  the HOST's, and the host reads nothing about themes.
* Only `accent` and `mode` were carried. The template has rendered `background`
  and `foreground` all along and they were dropped on the floor, which is why a
  themed button sat on the console's own violet chrome.

All three colours now reach the page, and a `[data-omarchy]` block in styles.css
expands them into every surface with `color-mix(in oklab, …)` — cards, hovers,
borders, the accent surface — plus the three tints of the lens mark, whose fills
were hard-coded violet and now follow the brand tokens. One block covers light
and dark: `:root[data-omarchy]` outranks both `:root` and `.dark`, and the mixes
take their direction from the theme's own background/foreground pair.

`--success` / `--warning` / `--destructive` deliberately do not follow: they
encode meaning, and a theme whose accent is red must not leave "delete" and
"save" the same colour.

Every ratio was measured, not chosen: `web/tools/check-omarchy-palette.mjs`
reads them back out of the stylesheet, redoes the mixes and asserts WCAG
contrast across six shipped themes (Tokyo Night, Gruvbox, Nord, Catppuccin
Latte, Rose Pine Dawn, Everforest Light). It runs in `postbuild` beside
check-i18n, because nothing else in the repo can catch an unreadable palette —
the mixes resolve in the browser, so the typecheck, the tests and the build all
pass on one. Everforest Light is in that table on purpose: its own foreground is
5.2:1 against its own background, which is what caps a derived muted colour at
3.8:1 there. That ceiling is recorded in the stylesheet.

## Verification

* pf-vdisplay: 273 tests pass on linux-gnu (docker), `xcheck.sh linux clippy`
  clean. The new `is_config_reload` test proved non-vacuous by relaxing the
  matcher to a `contains` and watching it fail.
* web: build + `tsc --noEmit` + 37 tests + biome all clean. The theme-file
  validator test proved non-vacuous by dropping the background check; the
  palette gate proved non-vacuous by re-tuning a ratio.
* On glass: the trigger chain confirmed on the Omarchy box (`omarchy-theme-set`
  → `omarchy-restart-hyprctl` → `hyprctl reload`), `configreloaded` present in
  the Hyprland 0.56.2 binary, and the event socket found and connectable at
  exactly the path the new code computes. The built console served the box's
  live theme end to end over `/_auth/ui-config`.
* NOT yet verified on glass: the re-apply itself. A stream was live on the box
  and a `hyprctl reload` would have disrupted it.
2026-08-28 23:38:08 +02:00