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
Owner

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.

The palette gate

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.

Tuning against that table caught three real defects in the first draft: muted text at 2.96:1 on Catppuccin Latte, button text at 3.92:1 on Rose Pine Dawn, and the lens mark collapsing to 1.05:1 internal separation on Tokyo Night.

Everforest Light is in the 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 rather than papered over.

Verification

  • pf-vdisplay — 273 tests pass on linux-gnu (docker), xcheck.sh linux clippy clean, cargo fmt --check 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.
  • check-docs-drift.sh — exit 0.
  • On glass — the trigger chain confirmed on the Omarchy box (omarchy-theme-setomarchy-restart-hyprctlhyprctl 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 verified

  • The re-apply itself. A stream was live on the box and a hyprctl reload would have disrupted it. One hyprctl reload during a stream is the whole check.
  • Screenshots. Playwright cannot launch on the dev Mac under Node 25 (heap OOM on chromium.launch()), so the palette was validated numerically rather than visually.

Known, left alone

  • The console still renders its own violet server-side and flips to the theme after hydration, so a light Omarchy theme flashes dark→light. Pre-existing, not introduced here; closing it needs SSR'd theming.
  • theme-color meta stays #6c5bf3 — the Omarchy webapp window already inherits Chromium's own theming.
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. ## The palette gate 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. Tuning against that table caught three real defects in the first draft: muted text at 2.96:1 on Catppuccin Latte, button text at 3.92:1 on Rose Pine Dawn, and the lens mark collapsing to 1.05:1 internal separation on Tokyo Night. Everforest Light is in the 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 rather than papered over. ## Verification * **pf-vdisplay** — 273 tests pass on `linux-gnu` (docker), `xcheck.sh linux clippy` clean, `cargo fmt --check` 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. * **`check-docs-drift.sh`** — exit 0. * **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 verified * **The re-apply itself.** A stream was live on the box and a `hyprctl reload` would have disrupted it. One `hyprctl reload` during a stream is the whole check. * **Screenshots.** Playwright cannot launch on the dev Mac under Node 25 (heap OOM on `chromium.launch()`), so the palette was validated numerically rather than visually. ### Known, left alone * The console still renders its own violet server-side and flips to the theme after hydration, so a light Omarchy theme flashes dark→light. Pre-existing, not introduced here; closing it needs SSR'd theming. * `theme-color` meta stays `#6c5bf3` — the Omarchy webapp window already inherits Chromium's own theming.
enricobuehler added 1 commit 2026-08-28 21:42:43 +00:00
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
55b1dc848d
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.
enricobuehler merged commit 95a2683aae into main 2026-08-28 22:00:15 +00:00
enricobuehler deleted branch worktree-omarchy-theme-live 2026-08-28 22:00:22 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#437