Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core"import { generateSyntax, resolveThemeDocument, themeModes, type ResolvedTheme } from "@opencode/theme/tui"import { allThemes, getOpenCodeTheme, isThemeSource, parseTheme, type ThemeDocumentSource } from "../theme"import { ansiToRgba } from "../theme/color"import { discoverThemes } from "../theme/discovery"import { generateSystem, terminalMode } from "../theme/system"import { configDirectories } from "../util/config-directories"import { dedupeWith } from "effect/Array"import type { EntryKind, RunTuiConfig } from "./types"
type Tone = { body: ColorInput start?: ColorInput}
export type RunEntryTheme = Record<EntryKind, Tone>
export type RunSplashTheme = { left: ColorInput right: ColorInput leftShadow: ColorInput}
export type RunFooterTheme = { actionSecondaryText: ColorInput actionFocusedBg: ColorInput actionFocusedText: ColorInput formfieldText: ColorInput formfieldFocusedBg: ColorInput formfieldFocusedText: ColorInput selection: ColorInput running: ColorInput question: ColorInput permission: ColorInput success: ColorInput link: ColorInput categorical: ColorInput[] warning: ColorInput error: ColorInput muted: ColorInput text: ColorInput shade: ColorInput surface: ColorInput pane: ColorInput border: ColorInput line: ColorInput}
export type RunBlockTheme = { text: ColorInput muted: ColorInput syntax?: SyntaxStyle diffRemoved: ColorInput diffAddedBg: ColorInput diffRemovedBg: ColorInput diffContextBg: ColorInput diffHighlightAdded: ColorInput diffHighlightRemoved: ColorInput diffLineNumber: ColorInput diffAddedLineNumberBg: ColorInput diffRemovedLineNumberBg: ColorInput}
export type RunTheme = { background: ColorInput footer: RunFooterTheme entry: RunEntryTheme splash: RunSplashTheme block: RunBlockTheme}
export const transparent = RGBA.fromValues(0, 0, 0, 0)
const ansiPalette = Array.from({ length: 256 }, (_, index) => RGBA.fromIndex(index, ansiToRgba(index)))const palettes = new WeakMap<CliRenderer, TerminalColors>()
function srgbToLinear(value: number): number { return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4}
function oklab(color: RGBA) { const r = srgbToLinear(color.r) const g = srgbToLinear(color.g) const b = srgbToLinear(color.b) const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b) const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b) const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b) return { l: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s, }}
function nearestIndexed(indexed: RGBA[], color: RGBA): RGBA { const target = oklab(color) const hit = indexed.reduce( (best, item) => { const sample = oklab(item) const dl = sample.l - target.l const da = sample.a - target.a const db = sample.b - target.b const dist = dl * dl * 2 + da * da + db * db return dist >= best.dist ? best : { dist, item } }, { dist: Number.POSITIVE_INFINITY, item: indexed[0]! }, ) return RGBA.clone(hit.item)}
function map( theme: ResolvedTheme, indexed: RGBA[], syntax?: SyntaxStyle, system = false,): RunTheme { // V1 system migration serializes colors; restore terminal defaults before quantizing scrollback. const exact = (color: RGBA) => { if (system && color.equals(theme.text.base)) return RGBA.defaultForeground(color) if (system && color.equals(theme.background.base)) return RGBA.defaultBackground(color) return color } const scrollback = (color: RGBA) => { const value = exact(color) return value.a === 0 || value.intent !== "rgb" ? value : nearestIndexed(indexed, value) } syntax?.getAllStyles().forEach((style, name) => { syntax.registerStyle(name, { ...style, fg: style.fg && scrollback(style.fg), bg: style.bg && scrollback(style.bg), }) })
return { background: RGBA.defaultBackground(theme.background.base), footer: { actionSecondaryText: exact(theme.text.action.secondary.base), actionFocusedBg: exact(theme.background.action.primary.focused), actionFocusedText: exact(theme.text.action.primary.focused), formfieldText: exact(theme.text.formfield.base), formfieldFocusedBg: exact(theme.background.formfield.focused), formfieldFocusedText: exact(theme.text.formfield.focused), selection: exact(theme.text.formfield.selected), running: exact(theme.hue.interactive[200]), question: exact(theme.hue.accent[200]), permission: exact(theme.hue.accent[200]), success: exact(theme.text.feedback.success.base), link: exact(theme.markdown.link), categorical: dedupeWith( theme.categorical.map((scale) => exact(scale[200])), (a, b) => a.equals(b), ), warning: exact(theme.text.feedback.warning.base), error: exact(theme.text.feedback.error.base), muted: exact(theme.text.muted), text: exact(theme.text.base), shade: exact(theme.background.raised.base), surface: exact(theme.background.raised.base), pane: exact(theme.background.raised.high), border: exact(theme.border.base), line: exact(theme.background.raised.high), }, entry: { system: { body: scrollback(theme.text.muted) }, user: { body: scrollback(theme.text.base) }, assistant: { body: scrollback(theme.markdown.text) }, reasoning: { body: scrollback(theme.text.muted) }, tool: { body: scrollback(theme.text.muted), start: scrollback(theme.text.muted) }, error: { body: scrollback(theme.text.feedback.error.base) }, }, splash: { left: nearestIndexed(indexed, theme.text.muted), right: nearestIndexed(indexed, theme.text.base), leftShadow: nearestIndexed(indexed, theme.background.raised.base), }, block: { text: scrollback(theme.text.base), muted: scrollback(theme.text.muted), syntax, diffRemoved: scrollback(theme.diff.text.removed), diffAddedBg: scrollback(theme.diff.background.added), diffRemovedBg: scrollback(theme.diff.background.removed), diffContextBg: scrollback(theme.diff.background.context), diffHighlightAdded: scrollback(theme.diff.highlight.added), diffHighlightRemoved: scrollback(theme.diff.highlight.removed), diffLineNumber: scrollback(theme.diff.lineNumber.text), diffAddedLineNumberBg: scrollback(theme.diff.lineNumber.background.added), diffRemovedLineNumberBg: scrollback(theme.diff.lineNumber.background.removed), }, }}
export const RUN_THEME_FALLBACK = map( resolveThemeDocument(parseTheme(getOpenCodeTheme()), "dark"), ansiPalette,)export const RUN_THEME_FALLBACK_LIGHT = map( resolveThemeDocument(parseTheme(getOpenCodeTheme()), "light"), ansiPalette,)
function monoTheme(mode: "dark" | "light"): RunTheme { const foreground = RGBA.defaultForeground(mode === "light" ? "#000000" : "#ffffff") const background = RGBA.defaultBackground(mode === "light" ? "#ffffff" : "#000000") return { background, footer: { actionSecondaryText: foreground, actionFocusedBg: background, actionFocusedText: foreground, formfieldText: foreground, formfieldFocusedBg: background, formfieldFocusedText: foreground, selection: foreground, running: foreground, question: foreground, permission: foreground, success: foreground, link: foreground, categorical: [foreground], warning: foreground, error: foreground, muted: foreground, text: foreground, shade: background, surface: background, pane: background, border: foreground, line: background, }, entry: { system: { body: foreground }, user: { body: foreground }, assistant: { body: foreground }, reasoning: { body: foreground }, tool: { body: foreground }, error: { body: foreground }, }, splash: { left: foreground, right: foreground, leftShadow: background }, block: { text: foreground, muted: foreground, diffRemoved: foreground, diffAddedBg: background, diffRemovedBg: background, diffContextBg: background, diffHighlightAdded: foreground, diffHighlightRemoved: foreground, diffLineNumber: foreground, diffAddedLineNumberBg: background, diffRemovedLineNumberBg: background, }, }}
export const RUN_THEME_MONO = monoTheme("dark")const RUN_THEME_MONO_LIGHT = monoTheme("light")
export async function resolveRunTheme( renderer: CliRenderer, config?: RunTuiConfig["theme"], mono = false,): Promise<RunTheme> { if (mono) { const mode = renderer.themeMode ?? (await renderer.waitForThemeMode(300)) ?? config?.mode return mode === "light" ? RUN_THEME_MONO_LIGHT : RUN_THEME_MONO }
const detected = await renderer.getPalette({ size: 256 }).catch(() => undefined) // A transient OSC timeout must not remap immutable scrollback against an unrelated ANSI palette. const colors = (detected?.defaultBackground ?? detected?.palette[0]) && (detected?.defaultForeground ?? detected?.palette[7]) ? detected : palettes.get(renderer) if (colors) palettes.set(renderer, colors) // Mini stays transparent: fresh OSC 11 wins, but cached colors must not override a new renderer mode. const mode = (detected && terminalMode(detected)) ?? renderer.themeMode ?? (colors && terminalMode(colors)) ?? (config?.mode === "light" ? "light" : "dark") const name = config?.name ?? "opencode" if (name === "system" && !colors) { return mode === "light" ? RUN_THEME_FALLBACK_LIGHT : RUN_THEME_FALLBACK }
const resolved = await themeSource(name, colors, mode) .then((source) => { const document = parseTheme(source, name) // Mini keeps the terminal background, so the opposite theme mode is not a safe fallback. if (themeModes(document).includes(mode)) return resolveThemeDocument(document, mode) }) .catch(() => undefined) const theme = resolved ?? resolveThemeDocument(parseTheme(getOpenCodeTheme()), mode) const indexed = colors ? ansiPalette.map((color, index) => (colors.palette[index] ? RGBA.fromIndex(index, colors.palette[index]!) : color)) : ansiPalette return { ...map(theme, indexed, generateSyntax(theme), name === "system" && resolved !== undefined), background: RGBA.defaultBackground(colors?.defaultBackground ?? theme.background.base), }}
async function themeSource( name: string, colors: TerminalColors | undefined, mode: "dark" | "light",): Promise<ThemeDocumentSource> { if (name === "system" && colors) return generateSystem(colors, mode) const { Global } = await import("@opencode/util/global") const custom = await discoverThemes( configDirectories(process.env.OPENCODE_CONFIG_DIR ?? Global.Path.config, process.cwd()), ) const source = custom[name] ?? allThemes()[name] ?? getOpenCodeTheme() return isThemeSource(source) ? source : getOpenCodeTheme()}