Uh oh!
There was an error while loading. Please reload this page.
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 into
Conversation
kavhnr
commented
Feb 25, 2026
@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)aspiers
commented
Mar 2, 2026
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