[PR #657] [MERGED] feat(renderer): add Mode 2031 dark/light theme detection #1467

Closed
opened 2026-03-14 09:38:27 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/anomalyco/opentui/pull/657
Author: @kavhnr
Created: 2/9/2026
Status: Merged
Merged: 2/10/2026
Merged by: @kommander

Base: mainHead: feat/mode-2031


📝 Commits (2)

  • 258bf2c feat(renderer): add Mode 2031 dark/light theme detection
  • 846e043 Merge branch 'main' into feat/mode-2031

📊 Changes

3 files changed (+41 additions, -3 deletions)

View changed files

📝 packages/core/src/renderer.ts (+25 -0)
📝 packages/core/src/types.ts (+3 -0)
📝 packages/core/src/zig/terminal.zig (+13 -3)

📄 Description

https://github.com/user-attachments/assets/b6b47db0-beb5-4c8d-8147-df6724dbf823

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


🔄 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/657 **Author:** [@kavhnr](https://github.com/kavhnr) **Created:** 2/9/2026 **Status:** ✅ Merged **Merged:** 2/10/2026 **Merged by:** [@kommander](https://github.com/kommander) **Base:** `main` ← **Head:** `feat/mode-2031` --- ### 📝 Commits (2) - [`258bf2c`](https://github.com/anomalyco/opentui/commit/258bf2cd56adc6344ea137e3fc02ea30dd8e884a) feat(renderer): add Mode 2031 dark/light theme detection - [`846e043`](https://github.com/anomalyco/opentui/commit/846e043fc281c0d8fa4f88a6cebcbf5daf8549bf) Merge branch 'main' into feat/mode-2031 ### 📊 Changes **3 files changed** (+41 additions, -3 deletions) <details> <summary>View changed files</summary> 📝 `packages/core/src/renderer.ts` (+25 -0) 📝 `packages/core/src/types.ts` (+3 -0) 📝 `packages/core/src/zig/terminal.zig` (+13 -3) </details> ### 📄 Description https://github.com/user-attachments/assets/b6b47db0-beb5-4c8d-8147-df6724dbf823 ## 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. 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](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/modes.zig)) - 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](https://github.com/helix-editor/helix/pull/14356) and [Contour spec](https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md) 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 ```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" }) ``` ## Changes - `packages/core/src/zig/terminal.zig` — `setColorSchemeUpdates()` helper, enable in `enableDetectedFeatures()`, use in `resetState()`, broaden DECRPM detection - `packages/core/src/renderer.ts` — `_themeMode` property, `themeModeHandler`, `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) - [Ghostty mode handling](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/modes.zig) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-14 09:38:27 +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#1467
No description provided.