Skip to content

feat(renderer): add Mode 2031 dark/light theme detection#657

Merged
kommander merged 2 commits intoanomalyco:mainfrom
kavhnr:feat/mode-2031
Feb 10, 2026
Merged

feat(renderer): add Mode 2031 dark/light theme detection#657
kommander merged 2 commits intoanomalyco:mainfrom
kavhnr:feat/mode-2031

Conversation

@kavhnr
Copy link
Contributor

@kavhnr kavhnr commented Feb 9, 2026

pr-657.MP4

Summary

Adds support for the Mode 2031 terminal protocol, enabling automatic dark/light theme detection and live reactivity to OS appearance changes.

Supersedes #654 (closed) — this is a clean rewrite addressing the review feedback to move setup/shutdown into the Zig native layer.

What this does

  • Subscribes to theme change notifications (CSI ? 2031 h) during enableDetectedFeatures() in terminal.zig
  • Queries initial theme mode (CSI ? 996 n) on startup
  • Parses CSI ? 997 ; 1 n (dark) / CSI ? 997 ; 2 n (light) responses via a new themeModeHandler in the input handler pipeline
  • Emits a theme_mode event on the renderer when the mode changes
  • Exposes a themeMode getter returning ThemeMode | null
  • Unsubscribes (CSI ? 2031 l) on destroy via resetState()
  • Exports a ThemeMode type from types.ts and adds theme_mode to RendererEvents

Supported terminals

  • Ghostty 1.0+ (source)
  • kitty 0.38.1+
  • Contour 0.4+
  • VTE 0.82+ (GNOME Terminal, etc.)

Unsupported terminals silently ignore the escape sequences — no fallback needed.

Design decisions

Each decision follows existing patterns in the codebase and/or established implementations in other terminal applications:

Decision Choice Justification
Setup/shutdown in Zig enableDetectedFeatures() / resetState() Matches how focus tracking (setFocusTracking) and bracketed paste (setBracketedPaste) are toggled — review feedback from #654
setColorSchemeUpdates() helper Extracted reusable method Follows setBracketedPaste, setFocusTracking, setModifyOtherKeys pattern exactly
Unconditional enable if (!self.state.color_scheme_updates) guard Same approach as bracketed paste (always enabled via hardcoded self.caps.bracketed_paste = true in checkEnvironmentOverrides). Mode 2031 is safe to send unconditionally — unsupported terminals ignore it
DECRPM detection: both 2031;1$y and 2031;2$y 1 = mode set (active), 2 = mode recognized Consistent with how focus_tracking (1004), sync (2026), and bracketed_paste (2004) all match both response codes in processCapabilityResponse()
Initial detection via CSI ? 996 n Simpler than DECRQM Matches Helix and Contour spec approach
Dedup guard Only emit when mode actually changes Avoids redundant redraws
Handler placement After focusHandler, before key handler Same priority tier as other terminal state handlers (capabilityHandler, focusHandler)
Default mode null Consumer decides fallback; avoids assuming dark or light
ThemeMode type in types.ts Named type export Consistent with other type exports like CursorStyle, WidthMethod

Usage

const renderer = await createCliRenderer()

// Read current mode (null if terminal hasn't responded yet)
renderer.themeMode // "dark" | "light" | null

// React to OS appearance changes
renderer.on("theme_mode", (mode) => {
  // mode is "dark" or "light"
})

Changes

  • packages/core/src/zig/terminal.zigsetColorSchemeUpdates() helper, enable in enableDetectedFeatures(), use in resetState(), broaden DECRPM detection
  • packages/core/src/renderer.ts_themeMode property, themeModeHandler, themeMode getter
  • packages/core/src/types.tsThemeMode type, theme_mode event in RendererEvents

References

Add support for the Mode 2031 terminal protocol for automatic dark/light
theme detection. This allows applications to reactively respond to OS
appearance changes in supported terminals (Ghostty 1.0+, kitty 0.38.1+,
Contour 0.4+, VTE 0.82+).

Zig (terminal.zig):
- Add setColorSchemeUpdates() helper matching setBracketedPaste/setFocusTracking pattern
- Use setColorSchemeUpdates() in resetState() instead of inline write + state flip
- Enable color scheme updates in enableDetectedFeatures() with initial query
- Recognize both 2031;1$y (mode active) and 2031;2$y (mode recognized)
  in processCapabilityResponse(), matching how focus_tracking/sync/bracketed_paste
  already handle both DECRPM response codes

TypeScript (renderer.ts):
- Add themeModeHandler parsing CSI ? 997 ; {1|2} n responses
- Emit theme_mode event with dedup guard (only when mode changes)
- Expose themeMode getter returning ThemeMode | null

Types (types.ts):
- Export ThemeMode type
- Add theme_mode to RendererEvents interface
@kavhnr kavhnr marked this pull request as ready for review February 9, 2026 21:34
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 10, 2026

@opentui/core

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core@846e043

@opentui/react

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/react@846e043

@opentui/solid

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/solid@846e043

@opentui/core-darwin-arm64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@846e043

@opentui/core-darwin-x64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@846e043

@opentui/core-linux-arm64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@846e043

@opentui/core-linux-x64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@846e043

@opentui/core-win32-arm64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@846e043

@opentui/core-win32-x64

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@846e043

commit: 846e043

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.

2 participants