From e50f77bbbcf0338a6f2ff4a2046e20fba69ad961 Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+justinlevinedotme@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:02:25 -0800 Subject: [PATCH 1/2] feat(tui): add system dark/light mode matching Add option to automatically sync TUI theme mode with OS appearance. Polls OS dark mode setting every 3 seconds when enabled. - Add getOSDarkMode() for cross-platform detection (macOS, Windows, Linux) - Add 'Match system dark/light mode' toggle in command list - Persist setting to kv.json as auto_mode_enabled (off by default) Closes #9697 --- packages/opencode/src/cli/cmd/tui/app.tsx | 103 +++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4b177e292cf..b0e5ef68265 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,7 +2,19 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" -import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" +import { + Switch, + Match, + createEffect, + untrack, + ErrorBoundary, + createSignal, + onMount, + batch, + Show, + on, + onCleanup, +} from "solid-js" import { Installation } from "@/installation" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -99,6 +111,62 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { import type { EventSource } from "./context/sdk" +async function getOSDarkMode(): Promise<"dark" | "light"> { + const { execSync } = await import("child_process") + + const run = (cmd: string) => { + try { + return execSync(cmd, { encoding: "utf-8", timeout: 1000, stdio: ["pipe", "pipe", "pipe"] }).trim() + } catch { + return "" + } + } + + if (process.platform === "darwin") { + const result = run("defaults read -g AppleInterfaceStyle") + return result.toLowerCase() === "dark" ? "dark" : "light" + } + + if (process.platform === "win32") { + const result = run( + "reg query HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize /v AppsUseLightTheme", + ) + // Output contains "AppsUseLightTheme REG_DWORD 0x0" (0 = dark, 1 = light) + const match = result.match(/AppsUseLightTheme\s+REG_DWORD\s+0x(\d)/) + if (match) return match[1] === "0" ? "dark" : "light" + return "dark" + } + + if (process.platform === "linux") { + // Try GNOME + const gnome = run("gsettings get org.gnome.desktop.interface color-scheme") + if (gnome.includes("prefer-dark")) return "dark" + if (gnome.includes("prefer-light") || gnome.includes("default")) return "light" + + // Try KDE + const kde = run("kreadconfig5 --group General --key ColorScheme") + if (kde.toLowerCase().includes("dark")) return "dark" + if (kde) return "light" + } + + // Fallback to terminal detection + return getTerminalBackgroundColor() +} + +async function getInitialMode(): Promise<"dark" | "light"> { + // Check if auto mode is enabled by reading KV file directly + try { + const { Global } = await import("@/global") + const path = await import("path") + const file = Bun.file(path.join(Global.Path.state, "kv.json")) + const kv = await file.json() + if (kv?.auto_mode_enabled) return getOSDarkMode() + } catch { + // KV file doesn't exist or is invalid, use terminal detection + } + return getTerminalBackgroundColor() +} + export function tui(input: { url: string args: Args @@ -109,7 +177,7 @@ export function tui(input: { }) { // promise to prevent immediate exit return new Promise(async (resolve) => { - const mode = await getTerminalBackgroundColor() + const mode = await getInitialMode() const onExit = async () => { await input.onExit?.() resolve() @@ -206,6 +274,7 @@ function App() { renderer.clearSelection() } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) + const [autoModeEnabled, setAutoModeEnabled] = createSignal(kv.get("auto_mode_enabled", false)) createEffect(() => { console.log(JSON.stringify(route.data)) @@ -233,6 +302,21 @@ function App() { } }) + // Poll for OS dark mode changes when auto mode is enabled + createEffect(() => { + if (!autoModeEnabled()) return + + const sync = async () => { + const osMode = await getOSDarkMode() + if (osMode !== mode()) setMode(osMode) + } + + // Sync immediately, then poll every 3 seconds + sync() + const interval = setInterval(sync, 3000) + onCleanup(() => clearInterval(interval)) + }) + const args = useArgs() onMount(() => { batch(() => { @@ -468,6 +552,21 @@ function App() { }, category: "System", }, + { + title: autoModeEnabled() ? "Stop matching system dark/light mode" : "Match system dark/light mode", + value: "theme.auto_mode", + onSelect: async (dialog) => { + const next = !autoModeEnabled() + setAutoModeEnabled(next) + kv.set("auto_mode_enabled", next) + if (next) { + const osMode = await getOSDarkMode() + setMode(osMode) + } + dialog.clear() + }, + category: "System", + }, { title: "Help", value: "help.show", From 8256065f9d993844249a10b2e4b809693e2b3bfb Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+justinlevinedotme@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:21:45 -0800 Subject: [PATCH 2/2] fix: random formatting change in `app.tsx` --- packages/opencode/src/cli/cmd/tui/app.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index b0e5ef68265..46121438070 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,19 +2,7 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" -import { - Switch, - Match, - createEffect, - untrack, - ErrorBoundary, - createSignal, - onMount, - batch, - Show, - on, - onCleanup, -} from "solid-js" +import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on, onCleanup } from "solid-js" import { Installation } from "@/installation" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog"