protocol, grid: answer OSC 4 palette queries from the window's configured palette #165

Merged
natsukium merged 2 commits from theme/palette-report into main 2026-10-18 14:24:59 +09:00
Owner

Closes #159.

Why

OSC 4 ; n ; ? answered the program's own override or else the compiled-in xterm palette, while a window with [theme.palette] / [theme.palette.indexed] paints other colors, so a program that reads the palette to pick readable colors (Claude Code does) picked against colors nobody sees. An embedder driving felis-grid without a daemon asked for the same on the GitHub mirror.

What

  • grid, daemon: RIS fix (first commit). RIS rebuilt the grid and dropped host state: the configured fg/bg/cursor and the OS light/dark preference (so OSC 11 ; ? answered xterm white after reset) and the PTY pixel winsize. It also dropped the program's OSC 4 / OSC 10/11/12 overrides without arming their deltas, so a window kept painting colors no query answered. RIS now carries the host state across and ships PaletteResetAll / ThemeColor Reset exactly when an override existed.
  • grid: set_palette_config_default / palette_config_default, a layer between the program's override and the compiled-in table; OSC 104 and RIS fall back to it. Kept out of the upgrade dump (a restored session re-resolves it from its windows).
  • protocol (minor 2): SessionConfigureTheme.palette, sparse ConfiguredPaletteEntry list, skippable (an older peer degrades to today's behavior). Duplicate or >255 index refused at decode; felis-json v1 mirrors it.
  • daemon: the palette rides the atomic theme report along the presentation chain: one window's palette answers, never stitched with a mirror's.
  • client: reports exactly the slots it paints; a palette-only config reload re-reports.
  • docs: REQ-900a, security-model carve-out, session-lifecycle, overview, ipc.md (ConfigureTheme, minor-ledger row 2), vt-compliance, CHANGELOG.

OSC 5 special colors are unchanged (no config key sets them).

Verification

  • just check passes; the first commit alone passes nextest -p felis-grid -p felis-daemon --all-features.
  • Design reviewed by pi (sol) over three rounds (PASS on round 3), implementation reviewed by pi (sol) (PASS; its two non-blocking test suggestions are adopted).
Closes #159. ## Why `OSC 4 ; n ; ?` answered the program's own override or else the compiled-in xterm palette, while a window with `[theme.palette]` / `[theme.palette.indexed]` paints other colors, so a program that reads the palette to pick readable colors (Claude Code does) picked against colors nobody sees. An embedder driving felis-grid without a daemon asked for the same on the GitHub mirror. ## What - **grid, daemon: RIS fix (first commit).** RIS rebuilt the grid and dropped host state: the configured fg/bg/cursor and the OS light/dark preference (so `OSC 11 ; ?` answered xterm white after `reset`) and the PTY pixel winsize. It also dropped the program's `OSC 4` / `OSC 10/11/12` overrides without arming their deltas, so a window kept painting colors no query answered. RIS now carries the host state across and ships `PaletteResetAll` / `ThemeColor Reset` exactly when an override existed. - **grid:** `set_palette_config_default` / `palette_config_default`, a layer between the program's override and the compiled-in table; `OSC 104` and RIS fall back to it. Kept out of the upgrade dump (a restored session re-resolves it from its windows). - **protocol (minor 2):** `SessionConfigureTheme.palette`, sparse `ConfiguredPaletteEntry` list, skippable (an older peer degrades to today's behavior). Duplicate or >255 index refused at decode; felis-json v1 mirrors it. - **daemon:** the palette rides the atomic theme report along the presentation chain: one window's palette answers, never stitched with a mirror's. - **client:** reports exactly the slots it paints; a palette-only config reload re-reports. - **docs:** REQ-900a, security-model carve-out, session-lifecycle, overview, ipc.md (`ConfigureTheme`, minor-ledger row 2), vt-compliance, CHANGELOG. OSC 5 special colors are unchanged (no config key sets them). ## Verification - `just check` passes; the first commit alone passes `nextest -p felis-grid -p felis-daemon --all-features`. - Design reviewed by pi (sol) over three rounds (PASS on round 3), implementation reviewed by pi (sol) (PASS; its two non-blocking test suggestions are adopted).
RIS rebuilt the grid from `Grid::new` and carried only the terminal name,
the graphics tracker and the effect queue across. Two kinds of state went
wrong with it.

The client-derived layer was dropped: the configured fg/bg/cursor and the
OS light/dark preference are host state the daemon sets on attach, not VT
state, so after `ESC c` an `OSC 11 ; ?` answered xterm white and
`DSR ? 996 n` answered light until the window attached again, inverting
any colorscheme detector run right after `reset`. The PTY's pixel
winsize was zeroed the same way, so `CSI 14/16 t` and pixel mouse reports
read 0 until the next resize. All three now survive RIS beside the
terminal name.

The program's `OSC 4` and `OSC 10/11/12` overrides were dropped without
arming their deltas, so the fan-out shipped nothing and an attached
window kept painting colors no query answered any more. RIS now clears
them through the same setters `OSC 104` / `OSC 110-112` use before the
rebuild and carries the armed deltas across it, so a window is sent
`PaletteResetAll` and `ThemeColor Reset` exactly when an override
existed.

Assisted-by: Claude Code
protocol, grid, daemon, client, docs: answer OSC 4 queries from the window's palette
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m12s
windows / detect relevant changes (pull_request) Successful in 13s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 4m5s
windows / frontend smoke (Windows) (pull_request) Successful in 1m24s
bench / instruction-count regression gate (pull_request) Successful in 58s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 56s
pr / nix flake check (pull_request) Successful in 50s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m49s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 38s
windows / cargo clippy (Windows cross) (pull_request) Successful in 17s
239d0303c1
An `OSC 4 ; n ; ?` query answered the program's own override or else the
compiled-in xterm table, while a window with `[theme.palette]` or
`[theme.palette.indexed]` paints other colors. A program that reads the
palette to pick readable colors picked against colors nobody sees. An
embedder that drives felis-grid without a daemon and draws its own
palette asked for the same thing on the GitHub mirror.

`OSC 10/11/12 ; ?` already answer from the window's configured colors,
reported in `ConfigureTheme` and resolved along the presentation
candidate chain. The palette now rides the same report as a sparse list
of the slots the config sets, so it resolves with the trio as one atomic
report: a query never sees a palette stitched from two mirrors, and a
slot the winning window leaves out answers the compiled-in table rather
than another window's. The recorded rejection of a per-window palette
still holds; the chain picks one window, as it does for fg/bg.

felis-grid gains `set_palette_config_default`, the layer between the
program's override and the compiled-in table, which an embedder calls
directly. `OSC 104` and RIS drop only the override, so a reset answers
what the window paints again, matching the renderer's fallback. The
layer stays out of the upgrade dump: a restored session re-resolves it
from its reconnecting windows, as it does the trio.

On the wire this is `SessionConfigureTheme.palette`, protocol minor 2,
since v0.2.0 shipped minor 1. It is skippable: an older daemon skips it
and answers the compiled-in palette as before, and an older window sends
none, so the client sends it to every peer ungated. A duplicate index or
one past 255 is refused at decode rather than given a last-wins meaning.
A config reload now re-sends the report when only the palette changed.

Assisted-by: Claude Code
natsukium scheduled this pull request to auto merge when all checks succeed 2026-10-18 14:23:59 +09:00
natsukium deleted branch theme/palette-report 2026-10-18 14:25:00 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!165
No description provided.