Skip to content

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

Closed
kavhnr wants to merge 2 commits intoanomalyco:mainfrom
kavhnr:feat/mode-2031-theme-detection
Closed

feat(renderer): add Mode 2031 dark/light theme detection#654
kavhnr wants to merge 2 commits intoanomalyco:mainfrom
kavhnr:feat/mode-2031-theme-detection

Conversation

@kavhnr
Copy link
Contributor

@kavhnr kavhnr commented Feb 9, 2026

ResponsiveTUI

Ghostty-capture-20-36-38.mp4

Summary

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

What this does

  • Subscribes to theme change notifications (CSI ? 2031 h) during setupTerminal()
  • 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 "dark" | "light" | null
  • Unsubscribes (CSI ? 2031 l) on destroy, before other cleanup
  • Exports a ThemeMode type from types.ts

Supported terminals

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

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

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"
})

Design decisions

The implementation follows patterns established by Neovim (PR #31350) and Helix (PR #14356):

Decision Choice Rationale
Initial detection CSI ? 996 n query Simpler than DECRQM, matches Helix/Contour approach
Dedup guard Only emit when mode actually changes Avoids redundant redraws
Cleanup order Unsubscribe first in finalizeDestroy() Matches Neovim's terminfo_stop pattern — prevents stale notifications to subsequent processes
Write mechanism writeOut() Bypasses interceptStdoutWrite which captures but doesn't forward to the terminal
Handler placement After focusHandler, before key handler Same priority tier as other terminal state handlers
Default mode null Consumer decides fallback; avoids assuming dark or light

Changes

  • packages/core/src/renderer.ts_themeMode property, themeModeHandler, subscribe/unsubscribe lifecycle, 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+).

- Subscribe to theme notifications (CSI ? 2031 h) during setupTerminal
- Query initial theme mode (CSI ? 996 n) on startup
- Parse CSI ? 997 ; {1|2} n responses via themeModeHandler
- Emit 'theme_mode' event on the renderer when mode changes
- Expose themeMode getter returning 'dark' | 'light' | null
- Unsubscribe (CSI ? 2031 l) on destroy before other cleanup
- Export ThemeMode type from types.ts
@kavhnr kavhnr force-pushed the feat/mode-2031-theme-detection branch from a82566a to 15225a2 Compare February 9, 2026 03:30
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 9, 2026

@opentui/core

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/core@1a34130

@opentui/react

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/react@1a34130

@opentui/solid

npm i https://pkg.pr.new/anomalyco/opentui/@opentui/solid@1a34130

@opentui/core-darwin-arm64

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

@opentui/core-darwin-x64

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

@opentui/core-linux-arm64

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

@opentui/core-linux-x64

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

@opentui/core-win32-arm64

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

@opentui/core-win32-x64

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

commit: 1a34130


this.queryPixelResolution()

this.writeOut("\x1b[?2031h")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we do this in terminal.zig setup and shutdown sequences?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea we can/should. Moved to match how bracketed paste and focus tracking work. It only enables when the terminal actually reports support via DECRPM, which is how Ghostty handles mode activation in its state layer (https://github.com/ghostty-org/ghostty/blob/main/src/terminal/modes.zig) modes get toggled based on detected capability (not sent unconditionally)

Bonus: suspend/resume gets this for free. Updating now.

Move color scheme update (Mode 2031) enable/disable and initial query
from TypeScript renderer into the Zig terminal setup and shutdown
sequences, matching the existing pattern for focus tracking and
bracketed paste. Add setColorSchemeUpdates helper for consistency with
other mode toggles. The TS-side shutdown write was redundant as
resetState() already handled it.
@kavhnr kavhnr requested a review from kommander February 9, 2026 16:52
@kavhnr kavhnr marked this pull request as draft February 9, 2026 19:25
@kavhnr kavhnr closed this Feb 9, 2026
@kavhnr kavhnr deleted the feat/mode-2031-theme-detection branch February 9, 2026 20:38
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