a73x

docs/superpowers/plans/2026-09-05-native-appearance.md

Ref:   Size: 27.0 KiB   History

# Native appearance — incremental delivery

The user wants personalisation of muxg's fonts and colours, keeping its current
layout and navigation. Configuration takes effect on the next client launch;
hot reload is excluded because restarting the client is cheap and preserves
daemon sessions. Terminal CLI and browser appearance are outside this work.

## Agreed slices

1. **Theme the existing native UI with a hardcoded palette.** Establish one
   reusable theme value and demonstrate a visibly different, cohesive appearance
   before building configuration. Recorded demo approved on 2026-09-05.
2. **Config and fonts.** Read `$XDG_CONFIG_HOME/mux/config`, falling back to
   `~/.config/mux/config`; support Ghostty-compatible font-family and font-size
   (points), explicit CLI precedence, actionable diagnostics, and installed
   Nerd Font Mono icons verified in real output. Font fallback is separate scope.
3. **Ghostty theme files.** Load a theme by absolute path or name under mux's
   themes directory. Defaults < theme < explicit config < CLI. Preserve today's
   appearance when no config exists in the completed feature. Report unsupported
   appearance keys; document the supported subset. Demonstrate dark and light
   themes and an explicit colour override.
4. **Ligatures: deferred.** On 2026-09-06 the user chose to leave ligatures
   for now and review GUI/TUI parity instead. Existing shaping is per cell;
   cross-cell ligatures are not implied by font selection. Revisit scope only
   when the user chooses to resume this work.

Each slice ends with validated work and an actual application demo. User demo
acceptance is recorded separately from implementation and test results.

## Slice 1: hardcoded theme (recorded demo approved)

Use a warm dark trial theme with cream text, teal focus and amber bell feedback.
The original theme remains a named value for the later no-config default. The
trial is selected in source for this slice; there is no temporary CLI or config
interface to maintain. Theme extraction is the deliverable itself, and opening
inspection found no additional refactor needed before it.

Observable acceptance:

- Default terminal foreground/background and indexed colours use the theme;
  explicit application RGB values remain exact. Default inverse and extended
  indexed colours preserve their meaning.
- Multiple panes show themed focused/inactive headers, dividers and cursor,
  with no geometry, focus, input or lifecycle changes.
- Host/session pickers, recovery menus and the empty workspace use themed
  backgrounds, text and selected rows; bell feedback remains visible.
- The existing native cursor retains its block shape. Native terminal text
  selection does not yet exist; only existing popup selection is in this slice.
  Do not claim a text-selection demonstration or add unused selection settings.
- Verify the real framebuffer, including off-origin panes and explicit RGB
  output; record the running application on an isolated compositor.
- Run repository and relevant native delivery gates on frozen source. Record
  platform/renderer limits, failures and retained debt honestly.

Pair: Luna implementation owns native theme and painter changes; Terra reviews
adversarially; root owns integration probes, independent validation, recording,
documentation and commits. Requested models were confirmed in recorded session
turn-context metadata at startup.

## Delivery evidence

The review page source is
[`docs/demos/native-appearance-slice1.html`](../../demos/native-appearance-slice1.html).
[Private review page](https://charizard.folk-amberjack.ts.net/appearance-slice1/).
It brings the achievements, embedded demo, review prompts, validation and open
NVIDIA timing result together. The local staged page is
`dist/appearance-slice1/site/index.html`. After automatic review rejected the
initial Serve request, the user explicitly authorised sharing sprint pages and
demo assets on their own Tailscale across devices. The private HTTPS page and
video/preview range responses were verified. Remove only this route with
`tailscale serve --https=443 --set-path /appearance-slice1 off`, then stop the
owned loopback server using its recorded PID.

Chromium verified metadata (27.2 seconds, 1100×700), seeking and playback beyond
12 seconds, and desktop/mobile layouts without horizontal overflow. The local
server verified the canonical trailing-slash redirect, HTTP 206 prefix/suffix
video ranges, HTTP 416 for an invalid range and HTTP 404 for unlisted files.
Only the page, video and preview are served. The owned loopback server uses
`127.0.0.1:18771`; its PID is retained in
`dist/appearance-slice1/page-server.pid` for teardown. Browser evidence is in
`page-browser-check.json` and `page-desktop.png` / `page-mobile.png` beside it.

Implementation and adversarial review are complete. `src/gui/theme.zig` owns
native defaults, all 256 indexed entries, cursor and chrome colours. The painter
accepts one theme plus explicit foreground/background overrides for chrome rows;
application RGB remains independent. No native input or lifecycle code changed.

Closing cleanup removed unused generic chrome fields and wired bell foreground
as well as background. Review also corrected an early brief's cursor description:
the baseline is a full-cell block, and its geometry is preserved. Three older
red-glyph fixtures now request explicit RGB instead of assuming ANSI red must be
saturated enough to pass their placement thresholds.

Validation logs are retained under `dist/appearance-slice1/` in this worktree:

- `build-final.log`: pinned ReleaseSafe daemon/GUI pair and native units passed.
- `native-core.log`: ReleaseSafe native core passed.
- `ci.log`: full `make ci` passed, including checks, terminal integration,
  agent scenarios and throughput. `check-final.log` records the final precommit
  check after fixture and documentation updates.
- `native-e2e.log`: viewer, tiling, picker, resize, lifecycle and the new
  `test/native_theme.py` framebuffer acceptance all passed.
- `theme-offscreen.log` and `theme-wayland.log`: three real panes, all ANSI
  entries, extended indices, inverse defaults, explicit RGB, cursor, focused and
  inactive headers, dividers, host/recovery menus, bell expiry and empty workspace
  passed. Retained-frame capture does not trigger painting. PTY dimensions and
  surviving sessions are checked independently through the existing rig.
- `scale-wayland.log`: 200% → 100% → 150% → 200% scale transitions, stale-drag
  cancellation, nested resize, independent PTY dimensions and pixels passed.
- `stress-offscreen.log`: raw-output responsiveness passed. Wayland comparison
  results are recorded below; no performance improvement is claimed.

The 27.2-second `native-appearance-slice1.mp4` is a continuous recording of the
actual isolated Sway window at 200%, encoded H.264/yuv420p at 1100×700, without
audio or restart cuts. It shows multiple sessions, the indexed palette, explicit
RGB, focus, typing, picker and recovery highlights, bell feedback, resizing and
empty workspace. `native-appearance-slice1.png` is the retained preview. Root
inspected the preview and a decoded popup frame. The recording used the NVIDIA
compositor; its renderer log is retained as `compositor-nvidia.log`. The recorder
stopped before closing the owned GUI. No external publication was performed.

### Limits and follow-up

The initial native gate failed because its ANSI-red threshold assumed the old
palette; `native-e2e-before-fixture-fix.log` is retained. The corrected fixture
tests RGB glyph placement, while the new theme probe checks indexed colours.

An initial 14-row specimen resized the offscreen window from 960×600 to 1100×700;
upper capture regions read 0xaa. The cause is unproven. The final appearance
probe fits baseline 12-row panes; real Wayland resizing is covered separately.
Next renderer/offscreen-fixture work should investigate this if it needs growing
offscreen surfaces. Do not infer a Wayland resize failure from that observation.

The first Wayland stress setup raced a compositor mapping rule that chose
1000×650 against the lifecycle fixture's 960×600 expectation. The owned mapping
rule now uses the fixture's intended size. That pre-exercise failure is retained
as `stress-wayland-mapping-failure.log`.

On NVIDIA Wayland, the stress interactions completed but final frame p99 was
20,993 us against a 20,000 us budget, in the combined draw/readback/swap stage; this is a
failed budget result, retained as `stress-wayland-nvidia.log`. Offscreen passed.
The AMD comparison uses the same mode and scale, with CI and recording stopped.
It passed with final frame p99 16,022 us (`stress-wayland-amd.log`); renderer
provenance is retained in `compositor-amd.log`. Both reports and the offscreen
report are copied beside the logs. Input timings are sampled upper bounds with
5 ms polling. No new macOS validation is claimed.

### Matched NVIDIA comparison after user feedback

The user's display uses NVIDIA; the AMD run was only a headless cross-device
comparison and does not establish performance on that display. Root built the
unchanged `ad746c0f` baseline in a separate checkout and ran the same stress
script twice per version, interleaved on one owned RTX 3080 compositor. Both
pairs came from pinned ReleaseSafe builds. Output mode, scale, fixture and
logging were held constant; CI and recording had finished. Reports and binary
hashes are retained in `nvidia-comparison.json` and its per-run logs/JSON files.

| Run | Before reopen p99 (us) | Final p99 (us) | Input upper bound (ms) | Strict gate |
| --- | ---: | ---: | ---: | --- |
| Baseline 1 | 19,904 | 19,878 | 70.2 | pass |
| Theme 1 | 18,923 | 18,886 | 70.2 | pass |
| Baseline 2 | 18,822 | 19,935 | 70.3 | pass |
| Theme 2 | 20,136 | 20,884 | 60.4 | fail |

CPU rebuild p99 overlapped (225–278 us baseline, 225–269 us theme). The
combined draw/readback/swap stage was lower for the first theme pair and higher
for the second; no consistent appearance penalty was established. That does
**not** turn the above-budget NVIDIA result into a pass. The strict NVIDIA
stress gate remains unresolved, with no budget or refresh-rate changes made.
Input observations remained inside the independent 250 ms bound. The frame
metric includes test framebuffer readback and vsync and does not isolate driver,
compositor or render cost. Headless evidence does not establish physical-display
performance. This remains a separate renderer/validation follow-up; recorded appearance
acceptance below does not turn it into a passed gate.

The owned compositor and fixture processes were stopped; the temporary baseline
checkout was removed after its reports and hashes were retained.

The user accepted the recorded appearance demo on 2026-09-05: “I approve the
demo! Looks good.” The intentional teal focus was acknowledged. This is recorded
demo acceptance, not a claim of a separate hands-on trial or a waiver of the
NVIDIA stress limit. Config/fonts is the next authorised slice; Ghostty theme
file loading follows it. When adding config, restore `theme.legacy` as the no-config default and
make the theme probe explicitly select its trial fixture. Discuss ligatures only
after the preceding slices, as the user requested.

## Slice 2: config and fonts (recorded demo approved)

Goal: choose an installed font family and point size in mux's config, restart
the native client, and continue the same sessions with the new typography.
Ghostty theme file loading follows in slice 3; ligatures remain a later discussion.
Opening inspection found the existing font/cache boundary ready for the change;
no separate preliminary refactor is needed.

Observable acceptance:

- Read `$XDG_CONFIG_HOME/mux/config`, or `~/.config/mux/config` when unset.
  Missing configuration keeps the original 16 logical-pixel font and legacy
  theme. Unsupported keys and malformed values name the file and line.
- Accept `font-family` and fractional `font-size` points. Explicit CLI family
  and size override config; existing `--font-px` remains supported. Explain
  conflicting size flags and missing families without silently substituting.
- Preserve fractional sizing until final raster rounding and preserve the family
  at 200%, 100%, 150%, and back to 200% display scale. Match Ghostty's nominal
  point conversion (96 DPI on Linux, 72 on macOS), then apply SDL display scale.
- Demonstrate regular, bold, italic, and installed Nerd Font Mono icons in real
  output across multiple panes; check pixels and independent PTY dimensions.
- Edit the config while running: the existing client keeps its font. Restart it:
  saved panes, shell identities, and session output survive with the new font.
- Finish with required checks, adversarial review, closing cleanup, and a private
  review webpage with actual application video. Record acceptance separately.

Luna owns product/config/font code and focused units. Terra independently reviews
and sends concrete findings to Luna. Root owns integration scenarios, validation,
documentation, demo, webpage, and commits. Carry forward the unresolved NVIDIA
frame-budget result; do not count earlier appearance acceptance as a waiver.

### Slice 2 implementation and validation record

Config/fonts implementation, independent review and final functional validation
are complete. The user approved the recorded demo on 2026-09-06: “lgtm, continue to next sprint”. This does not imply a separate hands-on trial.
The parser supports one font-family and one font-size entry, optional family
quotes, blank lines and full-line comments. Duplicate keys are rejected. Missing
files use defaults; parse errors include the path and line, and font matching
errors name the requested family. CLI values override the matching config values,
including the original --font-px size flag.

Closing review removed early point rounding and redundant parser state, added
actual face validation after Fontconfig matching, and kept the font family through
cache rebuilds. The trial palette is now an explicit integration fixture gated
by MUXG_TEST_FIFO; regular launches use the original palette. No color-config
interface, font fallback, or ligature work was added.

Independent negative testing exposed Fontconfig copying a requested spacing into
a proportional match. The first FreeType fix rejected Noto Sans Mono because it
omits the fixed-width flag; baseline native units caught that. The final check
accepts that flag or equal unscaled printable-ASCII advances. It rejects an actual
installed proportional family without relying on small-size pixel rounding.
These observations and earlier compile errors are retained in the build logs.

The first diagnostic recording attempt used a nested client in a daemon shell,
whose deliberately isolated config differed from the outer GUI config. The final
recorder passes the intended XDG_CONFIG_HOME explicitly. This was a fixture error.

Artifacts live under `dist/appearance-slice2/`. Both binaries were built together
in ReleaseSafe with pinned Zig. The compositor log identifies NVIDIA RTX 3080;
its owned HEADLESS-1 output uses 2200×1400 at 200%. No physical output was changed.
No new macOS validation is claimed. Existing NVIDIA frame-budget misses remain
historical failed checks and a renderer/validation follow-up, irrespective of
the current font demo or any passing run.

Final evidence:

- `build-face-final.log`: native build, font/painter units and native core passed.
  `ci.log`: full CI passed, including terminal integration, agent and throughput.
- `native-e2e-final.log`: full viewer, tiling, picker, resize, lifecycle, theme
  and new font acceptance passed after the final FreeType fix.
- `fonts-offscreen-final.log` / `fonts-wayland-final.log`: config and CLI
  precedence, malformed config and proportional-family refusal, HOME fallback,
  three saved panes on two daemons, shell PID/PTY survival, independently queried
  kernel dimensions, actual glyph rasters and distinct Nerd Font icons passed.
  The font-file oracle verifies nonzero distinct FreeType glyph IDs. NVIDIA
  200% → 100% → 150% → 200% retains the selected family and glyphs. Result JSON
  copies retain font path, expected raster size and raster signatures.
- `scale-wayland.log`: existing drag cancellation, nested resize, kernel PTYs and
  pixels passed; the final face fix was separately exercised by the font scale
  scenario above.
- `stress-results.json`: both NVIDIA raw-output runs passed on final ReleaseSafe
  binaries with CI, other fixtures and recording stopped. Default before/final
  frame p99: 18,982 / 19,473 us; configured CommitMono 12.4pt: 18,405 / 18,825 us.
  Input-to-painted sampled upper bounds: 70.3 / 55.5 ms, with 5 ms polling.
  The unchanged frame budget is 20,000 us and input budget 250 ms. The frame
  metric includes readback and vsync. This is current-build acceptance evidence
  on headless NVIDIA, not a performance improvement or physical-display claim.

The demo is 33.2 seconds, H.264/yuv420p at 1100×700, recorded from actual NVIDIA
Wayland windows at 200%, without audio. Two explicit restart cuts occur at 10.2s
and 25.8s; each segment is continuous. It shows original defaults, config saved
without reloading the existing client, CommitMono icons/styles, retained panes
and typing, the picker, bad-config diagnostics, and a temporary CLI size override.
The recording and scripts are retained in `dist/appearance-slice2/`. The review
page source is `docs/demos/native-appearance-slice2.html`.

Private handoff: https://charizard.folk-amberjack.ts.net/appearance-slice2/
The standing Tailscale authorization applies. Only this route is added; slice 1
remains available. The allowlisted loopback server is `127.0.0.1:18772`; its PID is
in `dist/appearance-slice2/page-server.pid`. Teardown removes only this route with
`tailscale serve --https=443 --set-path /appearance-slice2 off`, then stops that
owned server. Browser and HTTPS evidence are retained alongside the recording.

Next: deliver the authorised Ghostty theme-loading slice. Do not
start ligature implementation without the agreed later discussion.

Review-page verification passed: private HTTPS canonical URL and 206 byte ranges
for video/preview, with the prior slice still available. Chromium loaded the
33.2-second 1100×700 video, sought to 12 seconds and played beyond 12.5 seconds
using a simulated user gesture. Desktop 1280px and mobile 390px layouts had no
horizontal overflow; root inspected the mobile page and the decoded diagnostic
frame. The initial browser probe omitted a user gesture and autoplay policy
correctly refused playback; the page uses manual controls. `check-final.log`
records the precommit gate. Owned compositor/browser fixtures were stopped; the
allowlisted review-page server remains running.


## Active sprint: Ghostty theme files — 2026-09-06

Goal: copy a Ghostty theme into mux's themes directory (or name an absolute
path), restart muxg, and use the chosen colors across terminal panes and chrome.
The user approved slice 2's recorded demo and authorised this next sprint.
The user rebased the worktree onto the shared GUI install/release targets;
opening inspection confirmed the clean rebased branch and no preliminary
refactor is needed. Original user changes in the main checkout remain separate.

Acceptance:

- `theme = NAME` resolves under the config directory's `themes/`; absolute paths
  work. `--theme` chooses the theme for one launch. No bundled theme catalogue,
  automatic light/dark switching, includes, or hot reload is implied.
- Support `background`, `foreground`, `cursor-color`, and repeatable `palette`
  entries (decimal indices 0–255, six-digit hex with optional `#`). Defaults →
  selected theme → explicit config → CLI, independent of config line order.
- Derive headers, dividers, menus and bell colors from the final palette with
  readable light/dark chrome. No-config/font-only settings retain exact legacy
  colors. Application RGB remains exact and unspecified indices retain defaults.
- Missing themes and malformed supported values refuse startup with a useful
  file/line diagnostic. Unsupported theme keys warn and are ignored; unsupported
  mux config keys remain fatal. No text selection or cursor-text support is
  implied by importing those keys. Duplicate singleton keys/indices are refused.
- Use real saved panes on two daemons, verify shell identities and kernel PTY
  dimensions across dark/light/config/CLI restarts, plus framebuffer colors,
  off-origin panes, menus, and a NVIDIA Wayland scale round trip.
- Run required CI/native gates and NVIDIA stress separately from recording.
  Deliver a review webpage and actual GUI video with disclosed restart cuts.
  Carry forward historical NVIDIA timing misses and offscreen resize observations.

Luna owns product code and focused units; Terra independently reviews; root owns
integration, documentation, real validation and the private demo handoff. Ligature
scope remains a discussion after this slice's acceptance.

### Slice 3 implementation and review

The loader and native appearance integration are implemented. A copied theme is
read once at startup; the final merged palette is passed to the existing painter.
Colors use strict six-digit hex; decimal palette entries are independently
optional. Singleton and same-index duplicates in files are fatal. Repeated CLI
indices use the last value, matching the other CLI flags.

Chrome is derived only when a supported color is set. Focus uses palette 6;
bell feedback uses palette 3. Unfocused headers mix background toward foreground
by 20%, menus by 12%, and dividers by 45%. Selected menu backgrounds mix focus
toward terminal background by 60%. Focus/bell/selected text uses whichever of
black and white gives higher sRGB contrast. Other text uses terminal foreground.
A missing cursor override uses final foreground. No-config/font-only launches
keep the exact legacy tokens. The prior hardcoded trial remains an explicit
integration fixture.

Independent review caught a packed-channel narrowing overflow, a missing cursor
fallback, loose numeric parsing, inaccurate diagnostics, and a contrast helper
that computed white/black ratios but returned different colors. Root closing
cleanup fixed the last mismatch, removed redundant palette-pair storage, an
unused compatibility alias and no-op warning callback, and added normal-color
merge/contrast regression units. The failed focused unit expectation and its
passing rerun are retained in `dist/appearance-slice3/review-build.log`.

Both selected agent models were verified from recorded turn-context metadata;
available cumulative counters are retained with cached input identified. Agent
agreement was followed by real binary/framebuffer acceptance. The user’s turn
interruptions paused the pair; root explicitly resumed them when discovered.

The rebased `make install INSTDIR=dist/native-release BINDIR=dist/demo-bin` target
built both ReleaseSafe binaries without touching the user’s installed programs.
Native units/core, full native integration, offscreen theme acceptance, NVIDIA
Wayland theme acceptance and the retained scale/drag/resize scenario passed.
The new oracle checks three panes on two daemons, shell identities, kernel PTYs,
all ANSI colors and extended overrides, inverse defaults, explicit RGB, cursor,
light/dark chrome and menus, bell expiry, precedence, warnings and diagnostics.

The first full CI attempt failed in terminal `05_session`: `mux d stop --quic`
remained connected after its five-second deadline. This is retained in `ci.log`;
the investigation and prerequisite fix are recorded below.

The focused pre-fix retry passed, so the CI symptom was intermittent. Inspection
found shutdown closed QUIC client sinks quietly before listener cleanup; peers
could wait for idle timeout instead of receiving CONNECTION_CLOSE. Separate
prerequisite commit `693d038` now calls `closeAll()` before those sinks are freed.
The reviewer checked owned/borrowed listener ordering and callback lifetime.
`make check` and the real `05_session` group passed after the fix; both release
binaries were rebuilt through the install target. Full CI and native integration
passed on this final source. No timeout or acceptance budget was relaxed.

The actual GUI recording is 30.4 seconds, H.264/yuv420p at 1100×700, without
audio. It contains three continuous segments with restart cuts at 12.4s and
24.0s. Unmodified Catppuccin Mocha and Latte Ghostty files are retained with
source URLs and hashes under `dist/appearance-slice3/themes/`. The recording
shows both themes, palette output and explicit RGB, typing, host pickers,
unchanged running-client colors after config edits, a malformed-color diagnostic,
and a CLI background override. Root inspected the decoded dark picker,
diagnostic frame and final preview. The first recording's application RGB sample
inherited an unreadable text color on its dark swatch under Latte; the specimen
now explicitly requests white RGB text as well. This is a demo-output correction,
not a renderer change. Both recordings/logs are retained.


### Final validation and handoff

All required final gates passed. Evidence under `dist/appearance-slice3/`:

- `build-final.log` records the native units/core and initial final GUI build;
  `build-quic-final.log` records the rebuilt release pair after the prerequisite.
- `ci-final.log` passes check, full terminal integration, agent and throughput;
  `native-e2e-final.log` passes the entire native suite on final daemon sources.
- `theme-{offscreen,wayland}.log` and their result JSON record theme acceptance;
  `scale-wayland.log` records the retained scale/drag/resize/PTY/pixel scenario.
- `stress-results.json`: default before/final p99 17,806 / 17,842 us, configured
  Latte/Nerd Font 17,801 / 17,814 us. Sampled input upper bounds 55.4 / 70.3 ms.
  Default uses `make native-stress` paired build artifacts; configured uses the
  staged install pair. Limits remain 20,000 us and 250 ms with 5 ms polling.
  Timings include readback and vsync; no performance improvement or physical
  display claim is made. Both runs followed CI, other fixtures and recording.

The user approved slice 3 on 2026-09-06: “LGTM.” This records demo approval,
not a separate hands-on trial. Ligatures are deferred at the user’s request;
the next planning task is the GUI/TUI gap and retrospective audit. The private review page source is
`docs/demos/native-appearance-slice3.html`; its staged copy and allowlisted assets
are in `dist/appearance-slice3/site/`. Private route and browser verification are
recorded below after publication.


Private handoff: https://charizard.folk-amberjack.ts.net/appearance-slice3/
Standing Tailscale authorization was used; earlier routes remain available.
`page-private-check.json` verifies canonical HTTPS and 206 video/preview byte
ranges. Chromium verified the 30.4-second 1100×700 video, seeking to 12s and
playback beyond 12.5s, plus desktop 1280px/mobile 390px layouts without horizontal
overflow. Root inspected the mobile page and final diagnostic frame.

The owned browser and NVIDIA compositor were stopped. The allowlisted loopback
page server remains on `127.0.0.1:18773`; PID is recorded in
`dist/appearance-slice3/page-server.pid` (cleanup details in `cleanup.json`).
Teardown removes only this route with
`tailscale serve --https=443 --set-path /appearance-slice3 off`, then stops that
owned server. Preserve the other review routes. No user desktop outputs or
installed binaries were changed. The recorded demo was approved on 2026-09-06.