Skip to content

Send initial color scheme report when mode 2031 is enabled - #21

Merged
lawrencecchen merged 9 commits into
mainfrom
fix-decrpm-2031-color-scheme
Mar 28, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
fix-decrpm-2031-color-scheme

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 28, 2026 •

Copy link
Copy Markdown

Adds initial DSR 997 report when DECSET 2031 is received. Also ensures Termio DerivedConfig uses surface-level conditional state during config reloads.


Summary by cubic

Send the initial DSR 997 when DECSET 2031 is enabled and fix incorrect color scheme reporting after config reloads. Also adds cmux theme picker integration to the list_themes CLI for live preview and apply.

  • Bug Fixes

    • Mode 2031 reports the correct color scheme after config reloads by using each surface’s conditional state in Termio.
  • New Features

    • Immediately send DSR 997 when DECSET 2031 (report color scheme) is enabled.
    • Add cmux picker support in list_themes:
      • Read CMUX_THEME_PICKER_* env vars, write a managed theme override block, and restore on cancel.
      • Post macOS NSDistributedNotificationCenter reload notice to com.cmuxterm.themes.reload-config.
      • Live preview with target modes (both/light/dark), footer UI, and Enter to apply.
    • New cli-helper build step to build the Ghostty CLI helper.

Written for commit 41eeaaf. Summary will update on new commits.

When changeConditionalState returns null (no theme-conditional config
rules in the user's config), the Termio DerivedConfig inherits its
conditional_state from the app-level config, which defaults to .light
and is never updated. This caused colorSchemeReportLocked to always
report light mode even when the surface's own color scheme was dark.

Fix: after creating the Termio DerivedConfig, override its
conditional_state with the surface's own config_conditional_state.
This ensures the DECRPM mode 2031 response matches the actual surface
color scheme.
Per the mode 2031 spec, the terminal must send an initial DSR 997
report when the mode is enabled via DECSET. Without this, clients
that enable mode 2031 never receive the current color scheme.
@lawrencecchen
lawrencecchen merged commit cf6c326 into main Mar 28, 2026
41 of 43 checks passed
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@lawrencecchen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 14 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 18 minutes and 14 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3780b04a-3cf1-447a-9f6f-9325716326f5

📥 Commits

Reviewing files that changed from the base of the PR and between 96bad90 and 41eeaaf.

📒 Files selected for processing (5)
  • build.zig
  • src/Surface.zig
  • src/cli/list_themes.zig
  • src/main_ghostty.zig
  • src/termio/stream_handler.zig
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-decrpm-2031-color-scheme

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/cli/list_themes.zig">

<violation number="1" location="src/cli/list_themes.zig:847">
P2: `kp_down` is handled by both the single-step and page-down branches, causing double movement and duplicate preview updates.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/cli/list_themes.zig
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{}))
try self.applyCmuxSelectionForCurrentTheme();
}
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) {

@cubic-dev-ai cubic-dev-ai Bot Mar 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: kp_down is handled by both the single-step and page-down branches, causing double movement and duplicate preview updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/list_themes.zig, line 847:

<comment>`kp_down` is handled by both the single-step and page-down branches, causing double movement and duplicate preview updates.</comment>

<file context>
@@ -432,40 +803,71 @@ const Preview = struct {
-                        if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{}))
+                            try self.applyCmuxSelectionForCurrentTheme();
+                        }
+                        if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) {
                             self.down(20);
-                        if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{}))
</file context>
Suggested change
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) {
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_page_down }, .{})) {
Fix with Cubic

@greptile-apps

greptile-apps Bot commented Mar 28, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR delivers two distinct changes: (1) the core terminal emulation fix — sending an initial DSR 997 color-scheme report when DECSET 2031 is enabled, and ensuring the termio DerivedConfig always carries the surface-level conditional_state rather than a potentially stale app-level value; and (2) a substantial new feature in list-themes that integrates the Ghostty theme picker with the "cmux" third-party terminal app, including live config file rewriting and macOS NSDistributedNotification posting.

Key changes:

  • src/termio/stream_handler.zig: Adds a setMode case for .report_color_scheme that fires color_scheme_report { .force = true } when mode 2031 is enabled — consistent with how focus_event and in_band_size_reports work.
  • src/Surface.zig: After DerivedConfig.init, explicitly overwrites conditional_state with self.config_conditional_state. Needed because changeConditionalState can return null (no theme-conditional rules), leaving the config's embedded state at the app level rather than the surface level.
  • src/cli/list_themes.zig: Introduces CmuxThemePicker, which reads from CMUX_THEME_PICKER_* environment variables, supports live light/dark/both theme targeting, writes an atomic config block, and sends a reload notification via ObjC (macOS only).
  • src/main_ghostty.zig: Migrates to the buffered std.fs.File.stdout().writer() API and adds a flush() call.
  • build.zig: Exposes a cli-helper step that depends on exe.install_step.

Issues found:

  • CmuxThemePicker.initialTheme() is defined but has no callers — appears to be dead code.
  • removeManagedThemeOverride leaves an orphaned cmux_block_start marker in place when no matching cmux_block_end is found, which can cause the managed block to accumulate on repeated writes.
  • syncCmuxPreview copies the CmuxThemePicker struct by value on every navigation event when a *const pointer would suffice.
  • vaxis.Key.kp_down matches both the "move 1" and "page down / move 20" key sets (a pre-existing binding bug), which in cmux mode causes two sequential file writes and two NSDistributedNotification posts per keypress — kp_page_down was likely intended for the page-down binding.

Confidence Score: 5/5

Safe to merge; the terminal emulation fixes are correct and all remaining findings are P2 style/robustness suggestions in the cmux integration code.

The two core changes (stream_handler.zig and Surface.zig) are correct, well-scoped, and follow established patterns. The cmux integration in list_themes.zig is additive and only active when CMUX_THEME_PICKER_CONFIG is set, so it does not regress existing Ghostty behavior. All identified issues are P2 (dead code, minor robustness edge case, copy inefficiency, and a doubled kp_down side-effect from a pre-existing key binding collision).

src/cli/list_themes.zig — the kp_down double-binding and the removeManagedThemeOverride orphan-marker edge case are the most actionable items.

Important Files Changed

Filename Overview
src/termio/stream_handler.zig Adds initial DSR 997 report on DECSET 2031 enable, consistent with existing patterns for focus_event and in_band_size_reports; force=true is correct here.
src/Surface.zig Fixes stale conditional_state in termio DerivedConfig during config reloads by overriding with surface-level state after DerivedConfig.init; fix is correct and well-explained.
src/cli/list_themes.zig Large addition of CmuxThemePicker integration for live theme preview in cmux terminal; contains dead code (initialTheme), a struct-by-value copy inefficiency, a pre-existing double-binding that causes double file writes in cmux mode, and an edge case in removeManagedThemeOverride.
src/main_ghostty.zig Updates stdout writer to use a buffered interface with explicit flush, adapting to the newer Zig I/O API; correct and necessary.
build.zig Adds cli-helper build step depending on exe.install_step; intentional since the same binary serves both as the GUI app and CLI helper.

Sequence Diagram

sequenceDiagram
    participant App as TerminalApp
    participant SH as StreamHandler
    participant TM as TermioThread
    participant S as Surface
    App->>SH: DECSET 2031 enable mode
    SH->>SH: modes.set report_color_scheme true
    SH->>TM: color_scheme_report force=true
    TM->>TM: colorSchemeReportLocked reads conditional_state.theme
    TM->>App: ESC response dark or light
    S->>S: DerivedConfig.init from config
    S->>S: override conditional_state with surface state
    S->>TM: queueIo change_config
    TM->>TM: changeConfig with surface conditional_state
Loading

Comments Outside Diff (2)

  1. src/cli/list_themes.zig, line 209-214 (link)

    P2 Orphaned start marker left behind when end marker is absent

    If cmux_block_start is found but cmux_block_end is not present (e.g. a partially-written or manually-corrupted config file), the function breaks out of the loop and appends contents[cursor..] — which still includes the orphaned start marker and everything that follows it. The block is never removed.

    On the next write, writeCmuxThemeOverride would call removeManagedThemeOverride again, see the orphaned start marker, fail to find the end marker again, and append a fresh managed block after the dangling one. This leaves the file in an ever-growing inconsistent state.

    Consider skipping to the start position when the end marker is missing:

    const end_marker = std.mem.indexOfPos(u8, contents, start, cmux_block_end) orelse {
        // Orphaned start with no end — drop everything from the start marker onward.
        try result.appendSlice(alloc, contents[cursor..start]);
        break;
    };
  2. src/cli/list_themes.zig, line 541 (link)

    P2 syncCmuxPreview takes CmuxThemePicker by value instead of by const pointer

    syncCmuxPreview is declared as fn syncCmuxPreview(self: *Preview, cmux: CmuxThemePicker) !void, meaning the entire struct (7 fields including multiple slice headers) is copied onto the stack on every call. Given that it's invoked on every navigation event (arrow keys, mouse wheel, search input), this creates unnecessary copies.

    Both call sites already hold a by-value copy from self.cmux orelse return — consider passing a const pointer to avoid a second copy:

    fn syncCmuxPreview(self: *Preview, cmux: *const CmuxThemePicker) !void {

    and update callers:

    try self.syncCmuxPreview(&cmux);

Reviews (1): Last reviewed commit: "Send initial color scheme report when mo..." | Re-trigger Greptile

Comment thread src/cli/list_themes.zig
Comment on lines +84 to +91
fn initialTheme(self: *const CmuxThemePicker) ?[]const u8 {
return switch (self.target_mode) {
.both => if (eqlOptionalTheme(self.initial_light, self.initial_dark))
self.initial_light orelse self.initial_dark
else
self.initial_dark orelse self.initial_light,
.light => self.initial_light orelse self.initial_dark,
.dark => self.initial_dark orelse self.initial_light,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 initialTheme appears to be dead code

CmuxThemePicker.initialTheme() is defined but never called anywhere in list_themes.zig. The Preview.init function initializes cmux_preview_light/dark directly from value.initial_light/dark rather than going through this helper. If this method isn't used externally, it should be removed to avoid confusion.

Comment thread src/cli/list_themes.zig
Comment on lines +843 to +850
if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{})) {
self.down(1);
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{}))
try self.applyCmuxSelectionForCurrentTheme();
}
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) {
self.down(20);
if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{}))
try self.applyCmuxSelectionForCurrentTheme();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 vaxis.Key.kp_down double-binding causes two file writes per keypress in cmux mode

vaxis.Key.kp_down appears in both the j/+/down/kp_down/kp_add match (move 1) and the page_down/kp_down match (move 20). This is a pre-existing double-binding, but the PR makes it worse in cmux mode: a single kp_down keypress now triggers two separate applyCmuxSelectionForCurrentTheme() calls, leading to two config file writes and two NSDistributedNotification posts in rapid succession.

The page_down binding likely intended vaxis.Key.kp_page_down rather than vaxis.Key.kp_down:

Suggested change
if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{})) {
self.down(1);
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{}))
try self.applyCmuxSelectionForCurrentTheme();
}
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) {
self.down(20);
if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{}))
try self.applyCmuxSelectionForCurrentTheme();
}
if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_page_down }, .{})) {
self.down(20);
try self.applyCmuxSelectionForCurrentTheme();
}

lawrencecchen added a commit that referenced this pull request Mar 30, 2026
* Add cmux theme picker helper hooks

* Fix cmux theme picker preview writes

* Improve cmux theme picker footer contrast

* Respect system theme in cmux picker

* Skip theme detection in cmux picker

* Match Ghostty theme picker startup

* Harden cmux theme override writes

* Fix DECRPM mode 2031 reporting wrong color scheme after config reload

When changeConditionalState returns null (no theme-conditional config
rules in the user's config), the Termio DerivedConfig inherits its
conditional_state from the app-level config, which defaults to .light
and is never updated. This caused colorSchemeReportLocked to always
report light mode even when the surface's own color scheme was dark.

Fix: after creating the Termio DerivedConfig, override its
conditional_state with the surface's own config_conditional_state.
This ensures the DECRPM mode 2031 response matches the actual surface
color scheme.

* Send initial color scheme report when mode 2031 is enabled

Per the mode 2031 spec, the terminal must send an initial DSR 997
report when the mode is enabled via DECSET. Without this, clients
that enable mode 2031 never receive the current color scheme.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Mar 31, 2026
* Add cmux theme picker helper hooks

* Fix cmux theme picker preview writes

* Improve cmux theme picker footer contrast

* Respect system theme in cmux picker

* Skip theme detection in cmux picker

* Match Ghostty theme picker startup

* Harden cmux theme override writes

* Fix DECRPM mode 2031 reporting wrong color scheme after config reload

When changeConditionalState returns null (no theme-conditional config
rules in the user's config), the Termio DerivedConfig inherits its
conditional_state from the app-level config, which defaults to .light
and is never updated. This caused colorSchemeReportLocked to always
report light mode even when the surface's own color scheme was dark.

Fix: after creating the Termio DerivedConfig, override its
conditional_state with the surface's own config_conditional_state.
This ensures the DECRPM mode 2031 response matches the actual surface
color scheme.

* Send initial color scheme report when mode 2031 is enabled

Per the mode 2031 spec, the terminal must send an initial DSR 997
report when the mode is enabled via DECSET. Without this, clients
that enable mode 2031 never receive the current color scheme.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
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.

1 participant