Skip to content

Fix DECRPM mode 2031 reporting wrong color scheme - #20

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

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

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 28, 2026 •

Copy link
Copy Markdown

Fixes DECRPM mode 2031 reporting light when surface is dark. The Termio DerivedConfig inherited conditional_state from the app level (default .light) instead of the surface's own state.


Summary by cubic

Fixes DECRPM mode 2031 so each surface reports the correct light/dark scheme. Adds a cmux-aware theme picker with live preview and safe config overrides.

  • Bug Fixes

    • Use the surface’s conditional state for the terminal config so DECRPM 2031 matches the actual color scheme (no stale app-level .light default after reloads).
  • New Features

    • cmux mode in list_themes: reads env for config/targets, live-applies selections, reverts on cancel, applies on Enter, and cycles target (both/light/dark) with “t”.
    • macOS: posts a distributed reload notification via objc; uses atomic writes and managed blocks for overrides; adds a cli-helper build step and flushes helper usage output.

Written for commit c4cef10. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added cmux theme picker support with environment-based theme overrides.
    • Added keyboard shortcut to cycle between light and dark theme modes.
    • Enhanced interactive theme preview with new footer display and keybindings.
  • Bug Fixes

    • Fixed configuration state handling during theme updates.

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.
@lawrencecchen
lawrencecchen merged commit 96bad90 into main Mar 28, 2026
77 of 81 checks passed
@coderabbitai

coderabbitai Bot commented Mar 28, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91096ce6-590d-4194-b66e-9591f93bf5ee

📥 Commits

Reviewing files that changed from the base of the PR and between b9989c1 and c4cef10.

📒 Files selected for processing (4)
  • build.zig
  • src/Surface.zig
  • src/cli/list_themes.zig
  • src/main_ghostty.zig

📝 Walkthrough

Walkthrough

The pull request adds cmux theme picker integration to the interactive theme lister, establishes build-step ordering for the CLI helper artifact, ensures conditional state consistency in surface configuration updates, and implements buffered stdout output with explicit flushing in the main executable.

Changes

Cohort / File(s) Summary
Build & Execution Infrastructure
build.zig, src/main_ghostty.zig
Added cli-helper build step dependency on GhosttyExe installation; upgraded stdout output to use buffered writer with explicit flush before exit.
Core State Management
src/Surface.zig
Added forcible assignment of conditional_state to surface config after DerivedConfig initialization to ensure proper theme-conditional state propagation.
CLI Theme Picker with cmux Support
src/cli/list_themes.zig
Introduced CmuxThemePicker struct and helpers to manage theme overrides via environment variables and persistent file blocks. Enhanced interactive TUI with cmux target-mode cycling, dynamic preview/apply logic, footer status display, and macOS notification-center integration for theme reload signaling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A CLI theme picker now hops with grace,
cmux dances through the preview space,
Buffered outputs, state aligned just right,
Build steps ordered, futures shining bright! ✨

✨ 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.

@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR's stated goal — fixing DECRPM mode 2031 reporting the wrong color scheme — is addressed cleanly by a single line in src/Surface.zig that overrides termio_config_ptr.conditional_state with the surface's own config_conditional_state after the Termio.DerivedConfig is initialised. Without this, the DerivedConfig always inherits the app-level _conditional_state (defaulting to .light), causing DECRPM 2031 to lie about the surface's actual dark/light theme.

However, the PR also includes ~400 lines of new functionality for a "cmux" (third-party terminal multiplexer) theme-picker integration in src/cli/list_themes.zig, plus a new cli-helper build step and a buffered-stdout improvement in src/main_ghostty.zig — none of which are mentioned in the PR description. The cmux additions:

  • Introduce CmuxThemePicker, driven entirely by environment variables (CMUX_THEME_PICKER_CONFIG, CMUX_THEME_PICKER_BUNDLE_ID, etc.)
  • Write/restore a managed theme = … block in an arbitrary config file using atomic file writes
  • Send macOS NSDistributedNotification (com.cmuxterm.themes.reload-config) via the ObjC runtime to notify the host app
  • Add a live-preview UI overlay and keyboard shortcuts (t to cycle target mode, Enter to apply, q/Ctrl-C to cancel)

Memory ownership and error-handling in the new code look sound overall. Two minor correctness concerns are noted inline: a stale cmux_applied_* tracking path and a double-restore that can emit a redundant reload notification.

Confidence Score: 4/5

The core one-line fix is correct and safe; the large undescribed cmux feature addition warrants additional review before merging.

All findings are P2, which would normally yield 5/5. However, ~400 lines of new functionality (cmux integration, file I/O, ObjC notifications, new build step) are entirely absent from the PR description, making independent validation harder. The double-restore path can trigger a redundant macOS NSDistributedNotification, and the stale applied-state tracking causes unnecessary repeated work on every keystroke in an edge case. These are not blocking bugs, but the scope mismatch and lack of description warrant a 4/5 to prompt a conversation before merging.

src/cli/list_themes.zig — the cmux integration code is large, undocumented in the PR, and contains two minor logic issues worth addressing.

Important Files Changed

Filename Overview
src/Surface.zig One-line bug fix: overrides conditional_state in the freshly-created Termio.DerivedConfig with the surface's own config_conditional_state so DECRPM mode 2031 reflects the per-surface theme rather than the stale app-level default.
src/cli/list_themes.zig ~400-line addition of a first-party cmux theme-picker integration (env-var-driven config file management, atomic writes, macOS distributed notifications); memory ownership is sound but this feature is entirely unrelated to the stated DECRPM 2031 fix and lacks description in the PR body.
build.zig Adds a cli-helper build step that depends on the main exe install step — straightforward and correct.
src/main_ghostty.zig Switches the helper-CLI stdout from an unbuffered writer to a 1 KiB stack-buffered writer with an explicit flush() call — safe and correct.

Sequence Diagram

sequenceDiagram
    participant App as App (config_conditional_state=.light default)
    participant Surface as Surface.updateConfig()
    participant TernioDerived as Termio.DerivedConfig.init()
    participant TermioThread as Termio Thread (DECRPM 2031)

    Note over App,TermioThread: Bug path (before fix)
    App->>Surface: updateConfig(config)
    Surface->>TernioDerived: init(alloc, config)
    Note right of TernioDerived: conditional_state = config._conditional_state (app-level default = .light)
    TernioDerived-->>Surface: DerivedConfig{conditional_state=.light}
    Surface->>TermioThread: change_config(DerivedConfig)
    TermioThread-->>TermioThread: DECRPM 2031 reports light ❌

    Note over App,TermioThread: Fixed path (after fix)
    App->>Surface: updateConfig(config)
    Surface->>TernioDerived: init(alloc, config)
    TernioDerived-->>Surface: DerivedConfig{conditional_state=.light (app default)}
    Surface->>Surface: override conditional_state = self.config_conditional_state
    Note right of Surface: surface-level state (.dark or .light)
    Surface->>TermioThread: change_config(DerivedConfig)
    TermioThread-->>TermioThread: DECRPM 2031 reports correct scheme ✅
Loading

Reviews (1): Last reviewed commit: "Fix DECRPM mode 2031 reporting wrong col..." | Re-trigger Greptile

Comment thread src/cli/list_themes.zig
Comment on lines +756 to +782
try self.syncCmuxPreview(cmux);
}

fn syncCmuxPreview(self: *Preview, cmux: CmuxThemePicker) !void {
if (eqlOptionalTheme(self.cmux_preview_light, self.cmux_applied_light) and
eqlOptionalTheme(self.cmux_preview_dark, self.cmux_applied_dark))
{
return;
}

if (eqlOptionalTheme(self.cmux_preview_light, cmux.initial_light) and
eqlOptionalTheme(self.cmux_preview_dark, cmux.initial_dark))
{
try restoreCmuxThemeOverride(&cmux);
} else {
const raw_theme_value = (try encodeCmuxThemeValue(
self.allocator,
self.cmux_preview_light,
self.cmux_preview_dark,
)) orelse return;
defer self.allocator.free(raw_theme_value);
try writeCmuxThemeOverride(self.allocator, &cmux, raw_theme_value);
}

try postCmuxReloadNotification(self.allocator, cmux.bundle_id);
self.cmux_applied_light = self.cmux_preview_light;
self.cmux_applied_dark = self.cmux_preview_dark;

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 Early return skips cmux_applied_* update

In syncCmuxPreview, when encodeCmuxThemeValue returns null (both cmux_preview_light and cmux_preview_dark are null), the function returns early via orelse return after the early-equality guard has already passed — meaning the preview state genuinely differs from the applied state, but cmux_applied_light/cmux_applied_dark are never updated to reflect that. Every subsequent navigation key press will call syncCmuxPreview again, hit the same path, and return early again rather than short-circuiting on the equality check at the top.

While harmless in practice (no file is written and no notification is sent), the applied-state tracking stays permanently stale for this edge case. Consider syncing the applied fields before returning:

const raw_theme_value = try encodeCmuxThemeValue(
    self.allocator,
    self.cmux_preview_light,
    self.cmux_preview_dark,
) orelse {
    // Both themes are null — nothing to write; sync applied state so we
    // don't keep re-entering this branch on every event.
    self.cmux_applied_light = self.cmux_preview_light;
    self.cmux_applied_dark = self.cmux_preview_dark;
    return;
};

Comment thread src/cli/list_themes.zig
Comment on lines +604 to +606
errdefer self.restoreCmuxOriginal() catch {};
defer if (self.outcome == .cancel) {
self.restoreCmuxOriginal() catch {};

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 Double restore on error + cancel

In run(), if an error propagates out while self.outcome is still .cancel (the initial value), both errdefer and the conditional defer will call restoreCmuxOriginal(). Writing the same file twice is idempotent and harmless, but it sends an unnecessary postCmuxReloadNotification on the second call, which could cause the host application to reload its config twice for no reason.

Consider folding both into a single defer that checks outcome != .apply:

defer {
    if (self.outcome != .apply) {
        self.restoreCmuxOriginal() catch {};
    }
}

This naturally handles the error path since outcome is initialised to .cancel.

@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.

No issues found across 4 files

ianblenke pushed a commit to ianblenke/ghostty that referenced this pull request Mar 31, 2026
…ners

Re-apply CI fixes for GitHub-hosted runners
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