[PR #654] [CLOSED] feat(renderer): add Mode 2031 dark/light theme detection #687

Closed
opened 2026-03-02 23:47:41 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/anomalyco/opentui/pull/654
Author: @kavhnr
Created: 2/9/2026
Status: Closed

Base: mainHead: feat/mode-2031-theme-detection


📝 Commits (2)

  • 15225a2 feat(renderer): add Mode 2031 dark/light theme detection
  • 1a34130 refactor(terminal): move Mode 2031 setup/shutdown to terminal.zig

📊 Changes

3 files changed (+39 additions, -2 deletions)

View changed files

📝 packages/core/src/renderer.ts (+24 -0)
📝 packages/core/src/types.ts (+3 -0)
📝 packages/core/src/zig/terminal.zig (+12 -2)

📄 Description

ResponsiveTUI

https://github.com/user-attachments/assets/dd2351a8-0925-4228-8cf3-10579a61a741

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


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/anomalyco/opentui/pull/654 **Author:** [@kavhnr](https://github.com/kavhnr) **Created:** 2/9/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `feat/mode-2031-theme-detection` --- ### 📝 Commits (2) - [`15225a2`](https://github.com/anomalyco/opentui/commit/15225a2cc8eb368eee33d4c310c2651fb840c1ab) feat(renderer): add Mode 2031 dark/light theme detection - [`1a34130`](https://github.com/anomalyco/opentui/commit/1a3413082265814ef8d219ab7936ddb07239aa54) refactor(terminal): move Mode 2031 setup/shutdown to terminal.zig ### 📊 Changes **3 files changed** (+39 additions, -2 deletions) <details> <summary>View changed files</summary> 📝 `packages/core/src/renderer.ts` (+24 -0) 📝 `packages/core/src/types.ts` (+3 -0) 📝 `packages/core/src/zig/terminal.zig` (+12 -2) </details> ### 📄 Description ResponsiveTUI https://github.com/user-attachments/assets/dd2351a8-0925-4228-8cf3-10579a61a741 ## Summary Adds support for the [Mode 2031](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md) 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 ```ts 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](https://github.com/neovim/neovim/pull/31350)) and Helix ([PR #14356](https://github.com/helix-editor/helix/pull/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.ts` — `ThemeMode` type, `theme_mode` event in `RendererEvents` ## References - [Mode 2031 spec (Contour)](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md) - [Neovim implementation (PR #31350)](https://github.com/neovim/neovim/pull/31350) - [Helix implementation (PR #14356)](https://github.com/helix-editor/helix/pull/14356) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-02 23:47:41 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/opentui#687
No description provided.