Skip to content

Commit 104a895

Browse files
authored
Light mode (#3709)
1 parent f98e730 commit 104a895

File tree

9 files changed

+677
-580
lines changed

9 files changed

+677
-580
lines changed

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,63 @@ import { Session as SessionApi } from "@/session"
2929
import { TuiEvent } from "./event"
3030
import { KVProvider, useKV } from "./context/kv"
3131

32+
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
33+
return new Promise((resolve) => {
34+
let timeout: NodeJS.Timeout
35+
36+
const cleanup = () => {
37+
process.stdin.setRawMode(false)
38+
process.stdin.removeListener("data", handler)
39+
clearTimeout(timeout)
40+
}
41+
42+
const handler = (data: Buffer) => {
43+
const str = data.toString()
44+
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
45+
if (match) {
46+
cleanup()
47+
const color = match[1]
48+
// Parse RGB values from color string
49+
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
50+
let r = 0,
51+
g = 0,
52+
b = 0
53+
54+
if (color.startsWith("rgb:")) {
55+
const parts = color.substring(4).split("/")
56+
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
57+
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
58+
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
59+
} else if (color.startsWith("#")) {
60+
r = parseInt(color.substring(1, 3), 16)
61+
g = parseInt(color.substring(3, 5), 16)
62+
b = parseInt(color.substring(5, 7), 16)
63+
} else if (color.startsWith("rgb(")) {
64+
const parts = color.substring(4, color.length - 1).split(",")
65+
r = parseInt(parts[0])
66+
g = parseInt(parts[1])
67+
b = parseInt(parts[2])
68+
}
69+
70+
// Calculate luminance using relative luminance formula
71+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
72+
73+
// Determine if dark or light based on luminance threshold
74+
resolve(luminance > 0.5 ? "light" : "dark")
75+
}
76+
}
77+
78+
process.stdin.setRawMode(true)
79+
process.stdin.on("data", handler)
80+
process.stdout.write("\x1b]11;?\x07")
81+
82+
timeout = setTimeout(() => {
83+
cleanup()
84+
resolve("dark")
85+
}, 1000)
86+
})
87+
}
88+
3289
export function tui(input: {
3390
url: string
3491
sessionID?: string
@@ -38,7 +95,9 @@ export function tui(input: {
3895
onExit?: () => Promise<void>
3996
}) {
4097
// promise to prevent immediate exit
41-
return new Promise<void>((resolve) => {
98+
return new Promise<void>(async (resolve) => {
99+
const mode = await getTerminalBackgroundColor()
100+
42101
const routeData: Route | undefined = input.sessionID
43102
? {
44103
type: "session",
@@ -65,8 +124,12 @@ export function tui(input: {
65124
<RouteProvider data={routeData}>
66125
<SDKProvider url={input.url}>
67126
<SyncProvider>
68-
<ThemeProvider>
69-
<LocalProvider initialModel={input.model} initialAgent={input.agent} initialPrompt={input.prompt}>
127+
<ThemeProvider mode={mode}>
128+
<LocalProvider
129+
initialModel={input.model}
130+
initialAgent={input.agent}
131+
initialPrompt={input.prompt}
132+
>
70133
<KeybindProvider>
71134
<DialogProvider>
72135
<CommandProvider>
@@ -109,7 +172,7 @@ function App() {
109172
const sync = useSync()
110173
const toast = useToast()
111174
const [sessionExists, setSessionExists] = createSignal(false)
112-
const { theme } = useTheme()
175+
const { theme, mode, setMode } = useTheme()
113176
const exit = useExit()
114177

115178
useKeyboard(async (evt) => {
@@ -238,6 +301,14 @@ function App() {
238301
},
239302
category: "System",
240303
},
304+
{
305+
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
306+
value: "theme.switch_mode",
307+
onSelect: () => {
308+
setMode(mode() === "dark" ? "light" : "dark")
309+
},
310+
category: "System",
311+
},
241312
{
242313
title: "Help",
243314
value: "help.show",
@@ -251,7 +322,7 @@ function App() {
251322
value: "app.exit",
252323
onSelect: exit,
253324
category: "System",
254-
}
325+
},
255326
])
256327

257328
createEffect(() => {
@@ -335,7 +406,9 @@ function App() {
335406
paddingRight={1}
336407
>
337408
<text fg={theme.textMuted}>open</text>
338-
<text attributes={TextAttributes.BOLD}>code </text>
409+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
410+
code{" "}
411+
</text>
339412
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
340413
</box>
341414
<box paddingLeft={1} paddingRight={1}>

packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ export function DialogStatus() {
1414
return (
1515
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
1616
<box flexDirection="row" justifyContent="space-between">
17-
<text attributes={TextAttributes.BOLD}>Status</text>
17+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
18+
Status
19+
</text>
1820
<text fg={theme.textMuted}>esc</text>
1921
</box>
2022
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
2123
<box>
22-
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text>
24+
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
2325
<For each={Object.entries(sync.data.mcp)}>
2426
{([key, item]) => (
2527
<box flexDirection="row" gap={1}>
@@ -35,7 +37,7 @@ export function DialogStatus() {
3537
>
3638
3739
</text>
38-
<text wrapMode="word">
40+
<text fg={theme.text} wrapMode="word">
3941
<b>{key}</b>{" "}
4042
<span style={{ fg: theme.textMuted }}>
4143
<Switch>
@@ -52,7 +54,7 @@ export function DialogStatus() {
5254
</Show>
5355
{sync.data.lsp.length > 0 && (
5456
<box>
55-
<text>{sync.data.lsp.length} LSP Servers</text>
57+
<text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
5658
<For each={sync.data.lsp}>
5759
{(item) => (
5860
<box flexDirection="row" gap={1}>
@@ -67,17 +69,20 @@ export function DialogStatus() {
6769
>
6870
6971
</text>
70-
<text wrapMode="word">
72+
<text fg={theme.text} wrapMode="word">
7173
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
7274
</text>
7375
</box>
7476
)}
7577
</For>
7678
</box>
7779
)}
78-
<Show when={enabledFormatters().length > 0} fallback={<text>No Formatters</text>}>
80+
<Show
81+
when={enabledFormatters().length > 0}
82+
fallback={<text fg={theme.text}>No Formatters</text>}
83+
>
7984
<box>
80-
<text>{enabledFormatters().length} Formatters</text>
85+
<text fg={theme.text}>{enabledFormatters().length} Formatters</text>
8186
<For each={enabledFormatters()}>
8287
{(item) => (
8388
<box flexDirection="row" gap={1}>
@@ -89,7 +94,7 @@ export function DialogStatus() {
8994
>
9095
9196
</text>
92-
<text wrapMode="word">
97+
<text wrapMode="word" fg={theme.text}>
9398
<b>{item.name}</b>
9499
</text>
95100
</box>

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "@opentui/core"
1212
import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
1313
import { useLocal } from "@tui/context/local"
14-
import { SyntaxTheme, useTheme } from "@tui/context/theme"
14+
import { useTheme } from "@tui/context/theme"
1515
import { SplitBorder } from "@tui/component/border"
1616
import { useSDK } from "@tui/context/sdk"
1717
import { useRoute } from "@tui/context/route"
@@ -60,7 +60,7 @@ export function Prompt(props: PromptProps) {
6060
const history = usePromptHistory()
6161
const command = useCommandDialog()
6262
const renderer = useRenderer()
63-
const { theme } = useTheme()
63+
const { theme, syntax } = useTheme()
6464

6565
const textareaKeybindings = createMemo(() => {
6666
const newlineBindings = keybind.all.input_newline || []
@@ -86,9 +86,9 @@ export function Prompt(props: PromptProps) {
8686
]
8787
})
8888

89-
const fileStyleId = SyntaxTheme.getStyleId("extmark.file")!
90-
const agentStyleId = SyntaxTheme.getStyleId("extmark.agent")!
91-
const pasteStyleId = SyntaxTheme.getStyleId("extmark.paste")!
89+
const fileStyleId = syntax().getStyleId("extmark.file")!
90+
const agentStyleId = syntax().getStyleId("extmark.agent")!
91+
const pasteStyleId = syntax().getStyleId("extmark.paste")!
9292
let promptPartTypeId: number
9393

9494
command.register(() => {
@@ -315,9 +315,9 @@ export function Prompt(props: PromptProps) {
315315
const sessionID = props.sessionID
316316
? props.sessionID
317317
: await (async () => {
318-
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
319-
return sessionID
320-
})()
318+
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
319+
return sessionID
320+
})()
321321
const messageID = Identifier.ascending("message")
322322
let inputText = store.prompt.input
323323

@@ -680,7 +680,7 @@ export function Prompt(props: PromptProps) {
680680
onMouseDown={(r: MouseEvent) => r.target?.focus()}
681681
focusedBackgroundColor={theme.backgroundElement}
682682
cursorColor={theme.primary}
683-
syntaxStyle={SyntaxTheme}
683+
syntaxStyle={syntax()}
684684
/>
685685
</box>
686686
<box
@@ -691,7 +691,7 @@ export function Prompt(props: PromptProps) {
691691
></box>
692692
</box>
693693
<box flexDirection="row" justifyContent="space-between">
694-
<text flexShrink={0} wrapMode="none">
694+
<text flexShrink={0} wrapMode="none" fg={theme.text}>
695695
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
696696
<span style={{ bold: true }}>{local.model.parsed().model}</span>
697697
</text>
@@ -701,14 +701,14 @@ export function Prompt(props: PromptProps) {
701701
</Match>
702702
<Match when={status() === "working"}>
703703
<box flexDirection="row" gap={1}>
704-
<text>
704+
<text fg={theme.text}>
705705
esc <span style={{ fg: theme.textMuted }}>interrupt</span>
706706
</text>
707707
</box>
708708
</Match>
709709
<Match when={props.hint}>{props.hint!}</Match>
710710
<Match when={true}>
711-
<text>
711+
<text fg={theme.text}>
712712
ctrl+p <span style={{ fg: theme.textMuted }}>commands</span>
713713
</text>
714714
</Match>

0 commit comments

Comments
 (0)