feat(tui): add Mode 2031 theme detection with tri-state appearance mode#15089
feat(tui): add Mode 2031 theme detection with tri-state appearance mode#15089kavhnr wants to merge 1 commit intoanomalyco:devfrom
Conversation
1c78a6c to
daa9a8a
Compare
|
@RhysSullivan this is similar to your PR#7162 so might be up your ally to review/hopefully merge for people that use different modes. |
…er-mode theme config
- Detect OS dark/light mode changes in real-time via Mode 2031 (Ghostty, kitty, etc.)
- Add tri-state appearance selector (auto/dark/light) to theme dialog with arrow key cycling
- Support per-mode theme config in tui.json: { dark: "tokyonight", light: "github" }
- Extend DialogSelect with header and onKeyboard props
- Remove standalone Toggle appearance command (now integrated into theme dialog)
daa9a8a to
ef1bf8f
Compare
|
I've tested this and it works great! Thanks! |
|
Hi @kavhnr. Tested it and I agree, it works really well in Ghostty. I also tested it on Warp though, and it didn't work there because Mode 2031 never fires on Warp, Alacritty, WezTerm, or iTerm2, so I wanted to extend it a bit to cover those terminals. Tried What does work: Two places to change in + let mode2031Active = !!renderer.themeMode
const handler = (mode: "dark" | "light") => {
+ mode2031Active = true
setStore("detectedMode", mode)
if (active() === "system") {
renderer.clearPaletteCache()
resolveSystemTheme()
}
}
renderer.on("theme_mode", handler)
onCleanup(() => renderer.off("theme_mode", handler))Then after your existing // Sync initial mode if terminal already responded to Mode 2031 query.
if (renderer.themeMode) {
setStore("detectedMode", renderer.themeMode)
}
+
+ const osc11 = /\x1b\]11;rgba?:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/
+ const norm = (h: string) => parseInt(h, 16) / ((1 << (h.length * 4)) - 1)
+ let pending = false
+ const queryOsc11 = () => {
+ if (mode2031Active || store.mode !== "auto" || pending) return
+ pending = true
+ const unsub = renderer.subscribeOsc((seq: string) => {
+ const m = seq.match(osc11)
+ if (!m) return
+ unsub()
+ pending = false
+ const luminance = 0.299 * norm(m[1]) + 0.587 * norm(m[2]) + 0.114 * norm(m[3])
+ const detected: "dark" | "light" = luminance > 0.5 ? "light" : "dark"
+ if (detected === store.detectedMode) return
+ setStore("detectedMode", detected)
+ if (active() === "system") {
+ renderer.clearPaletteCache()
+ resolveSystemTheme()
+ }
+ })
+ ;(renderer as any).writeOut("\x1b]11;?\x07")
+ const t = setTimeout(() => { unsub(); pending = false }, 1000)
+ t.unref()
+ }
+ const poll = setInterval(queryOsc11, 2000)
+ poll.unref()
+ onCleanup(() => clearInterval(poll))The ordering doesn't matter functionally — the sync block runs synchronously at mount, the poll fires 2s later at the earliest. But placing it after keeps the intent clear: Mode 2031 path first, OSC 11 fallback below it. The only rough edge is the |
mode-2031-theme-detection.mp4
Issue for this PR
Closes #13232
Supersedes #13233 (closed to rebase on current
dev).Type of change
What does this PR do?
OpenTUI v0.1.78 added Mode 2031 dark/light theme detection (opentui#657). OpenCode is already on v0.1.79 (#13036) which includes these APIs, but doesn't use them. This PR integrates
renderer.themeMode/renderer.on("theme_mode", ...)into OpenCode's theme system.Three changes:
auto/dark/light) — replaces the binary toggle.autofollows OS via Mode 2031, falling back to OSC 11 luminance on unsupported terminals.dark/lightare manual overrides."theme": { "light": "github", "dark": "catppuccin" }alongside existing"theme": "opencode"(backward compatible).Mode 2031 is the same approach used by Neovim and Helix. Event-driven, zero overhead. Unsupported terminals silently ignore the sequences — no degradation.
Why Mode 2031 over the approaches in existing open PRs:
execSync("defaults read")/gsettingsevery 3sEdge cases handled:
detectedModestays at initial OSC 11 value. No degradation.activeThemeMode()falls back to"dark". Existing custom themes work identically.onCleanup(() => renderer.off("theme_mode", ...))prevents leaks.How did you verify your code works?
bun run typecheckpasses (16/16 packages, zero errors)"theme": "opencode"works,"theme": { "light": "github", "dark": "catppuccin" }worksauraandayu(single-palette) unaffected by mode changesChecklist