Skip to content

fix(src): detect light/dark theme inside sessions via OSC 11#92

Open
kylecarbs wants to merge 1 commit into
mainfrom
fix/theme-detection-osc11
Open

fix(src): detect light/dark theme inside sessions via OSC 11#92
kylecarbs wants to merge 1 commit into
mainfrom
fix/theme-detection-osc11

Conversation

@kylecarbs

Copy link
Copy Markdown
Member

Fixes #91.

Problem

Programs like nvim and helix can't detect the terminal's light/dark theme inside a boo session (reported by @ldb). On startup they enter the alternate screen and, in the same burst, query the background color with OSC 11 (ESC ]11;? BEL). That query never reaches the real terminal:

  • boo attach strips it along with the alt-screen tail it rides behind (the alt-screen filter discards everything after the screen toggle).
  • boo ui renders into a client-side terminal that never answers it.

The embedded ghostty-vt also treats OSC color queries as no-ops, so the daemon answered nothing and apps fell back to a default (usually dark).

Fix

Make the daemon the terminal authority for the background color:

  • Probe + report (client.zig, ui.zig): a client probes its real terminal's background once at startup (OSC 11), parses the reply, and reports the RGB to the daemon via a new bg_color protocol message, sent right after attach.
  • Cache + strip + answer (daemon.zig, window.zig, oscquery.zig): the daemon caches the color per window, strips OSC 11 queries from child output so the real terminal can't also answer (no double reply), and answers each stripped query itself. A query that arrives before any color is known is deferred and answered as soon as a client reports one.
  • Color-scheme DSR (window.zig): CSI ?996n is now answered from the background's luminance (Rec. 601), classifying light vs dark.

The probe sends attach first and yields to pending signals, so a slow or unanswered probe never delays a kill or the initial window size.

Compatibility

Mixed daemon/client versions degrade gracefully: an older peer ignores the unknown bg_color message and behavior is unchanged.

Known tradeoff

The daemon always strips OSC 11 queries and answers from the probed background. If the real terminal answers the startup probe slower than the 150 ms timeout, the late reply leaks into the session as input (harmless for alt-screen apps, which await it; minor stray bytes for a bare shell). This is a narrow, high-latency edge case; full main-loop interception would remove it at the cost of complexity and is deferred.

Testing

  • zig build test — 151 unit tests pass (oscquery filter, OSC 11 reply parser with BEL/ST and channel-width scaling, window answer/defer + color-scheme luminance, RGB payload roundtrip).
  • zig build test-integration — PTY integration tests pass.
  • zig fmt --check src/*.zig — clean.
  • End-to-end with PTY harnesses: an nvim-like alt-screen + OSC 11 burst receives the background reply under both boo attach and boo ui, including the cold-start deferral path (session queries before the ui reports a color).
Plan and decision log

Root cause (confirmed empirically with a PTY harness)

  • nvim/helix enter the alternate screen (CSI ?1049h) and then immediately query the background with OSC 11 (ESC ]11;? BEL) in one burst.
  • The daemon's alt-screen filter (altscreen.zig) discards everything after the screen toggle and feeds it via Window.feedDiscarded, so the query never reaches the real terminal.
  • The embedded ghostty-vt never answers OSC color queries (the .query case in colorOperation is a no-op) and the color_scheme effect was null, so the daemon answered nothing. The app times out and falls back to dark.
  • Both boo attach (raw passthrough) and boo ui (compositor) are affected.

Design (daemon is the terminal authority)

  1. Clients (attach + ui) probe the real terminal once at startup with OSC 11, parse the reply, and report the background RGB to the daemon via a new bg_color protocol message.
  2. The daemon stores the background per window and:
    • strips OSC 11 ? queries from child output (new oscquery.zig filter), answering exactly once with the stored color so the real terminal cannot also answer (no double reply);
    • answers the color-scheme DSR (CSI ?996n) via the now-implemented color_scheme effect, derived from background luminance.
  3. Cold start: a query that arrives before any client reported a color is recorded and answered as soon as the color arrives.

Files

  • src/protocol.zig: bg_color message + RgbPayload (encode/decode, luminance, isDark).
  • src/oscquery.zig: new stateful filter that strips OSC 11 queries and reports them (BEL/ST terminators, sequences split across feeds, near-misses pass through).
  • src/window.zig: store bg, color_scheme effect, answer + defer logic, run the OSC filter.
  • src/daemon.zig: handle bg_color; run the OSC filter in serviceWindow before passthrough.
  • src/client.zig: probe + OSC 11 reply parser, wired into attach (attach first, then probe).
  • src/ui.zig: probe once at startup, pass bg to each View.
  • src/main.zig: register oscquery.zig in the unit-test aggregator.

Generated by Coder Agents on behalf of @kylecarbs.

Programs like nvim and helix could not detect the terminal's light/dark
theme inside a boo session. On startup they enter the alternate screen
and, in the same burst, query the background color with OSC 11
(ESC ]11;? BEL). That query never reaches the real terminal: a `boo
attach` strips it along with the alt-screen tail it rides behind, and a
`boo ui` view renders into a client-side terminal that never answers it.
The embedded ghostty-vt also treats OSC color queries as no-ops, so the
daemon answered nothing and apps fell back to a default (usually dark).

Make the daemon the terminal authority for the background color:

- A client probes its real terminal's background once at startup (OSC
  11) and reports the RGB to the daemon via a new `bg_color` protocol
  message. Both `boo attach` and `boo ui` send it right after `attach`.
- The daemon caches the color per window, strips OSC 11 queries from
  child output (so the real terminal can't also answer and double-reply),
  and answers each stripped query itself. A query that arrives before any
  color is known is deferred and answered as soon as a client reports one.
- The color-scheme DSR (CSI ?996n) is answered from the background's
  luminance (Rec. 601), classifying light vs dark.

The probe sends `attach` first and yields to pending signals so a slow
or unanswered probe never delays a kill or the initial window size.
Mixed daemon/client versions degrade gracefully: an older peer ignores
the unknown `bg_color` message and behavior is unchanged.

Fixes #91.
@ldb

ldb commented Jun 24, 2026

Copy link
Copy Markdown

Confirming that this fixes the system theme issues.

I do have a question for my understanding though (and please excuse me if I totally misunderstand this):

  • boo attach strips it along with the alt-screen tail it rides behind (the alt-screen filter discards everything after the screen toggle).
  • boo ui renders into a client-side terminal that never answers it.

Why is the query stripped and discarded, and why can't the client just pass it on to the real terminal?
From what I understood, this fix is now specific to background colour and any other OSC queries would have to be implemented similarly, is that correct?
Why can't we just use OSC itself as the protocol here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

system theme is not detected correctly by processes running inside boo session

2 participants