Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439import { CliRenderEvents, SyntaxStyle, type TerminalColors } from "@opentui/core"import { useRenderer } from "@opentui/solid"import { generateSyntax, resolveThemeDocument, themeModes, type ResolvedTheme, type SurfaceName,} from "@opencode/theme/tui"import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, parseTheme, selectedForeground, setCustomThemes, setSystemTheme, subscribeThemes, upsertTheme, type Theme, type ThemeDocumentSource,} from "../theme"import { generateSystem, terminalMode } from "../theme/system"import { discoverThemes } from "../theme/discovery"import { createComponentTheme, type ComponentTheme } from "../theme/component"import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js"import { createStore, produce } from "solid-js/store"import { createSimpleContext } from "./helper"import { useConfig } from "../config"import { useStorageOptional } from "./storage"import { DevTools } from "../devtools"import { configDirectories } from "../util/config-directories"
const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" })export type ThemeError = { name: string; error: Error }type ThemeErrorHandler = (event: ThemeError) => void
function createThemeErrors() { let handler: ThemeErrorHandler | undefined let pending: ThemeError | undefined
return { emit(name: string, cause: unknown) { const event = { name, error: cause instanceof Error ? cause : new Error(String(cause)) } if (handler) { handler(event) return } pending = event }, onError(next: ThemeErrorHandler) { handler = next if (pending) { next(pending) pending = undefined } return () => { if (handler === next) handler = undefined } }, }}
const themeErrors = createThemeErrors()
export type ThemeSource = Readonly<{ discover(): Promise<Record<string, unknown>> subscribeRefresh?(refresh: () => void): () => void}>
export const createThemeSource = (config: string): ThemeSource => ({ async discover() { return discoverThemes(configDirectories(config, process.cwd())) }, subscribeRefresh(refresh) { process.on("SIGUSR2", refresh) return () => process.off("SIGUSR2", refresh) },})
export { discoverThemes } from "../theme/discovery"
export { DEFAULT_THEMES, addTheme, allThemes, generateSyntax, hasTheme, selectedForeground, upsertTheme, type Theme,} from "../theme"
const THEME_REFRESH_DELAYS = [250, 1000] as const
type State = { mode: "dark" | "light" lock: "dark" | "light" | undefined active: string ready: boolean}
type Themes = { current: ComponentTheme currentTokens: Accessor<ResolvedTheme> readonly selected: string all: typeof allThemes has: typeof hasTheme currentSyntax: Accessor<SyntaxStyle> mode: Accessor<"dark" | "light"> modes: Accessor<readonly ("dark" | "light")[]> supports(mode: "dark" | "light"): boolean locked: Accessor<boolean> lock(): void unlock(): void setMode(mode?: "dark" | "light", persist?: boolean): boolean set(theme: string): boolean afterPaint(): void onError(handler: ThemeErrorHandler): () => void readonly ready: boolean}
type ThemeContextValue = { current: ComponentTheme themes: Themes readonly ready: boolean}
const [store, setStore] = createStore<State>({ mode: "dark", lock: undefined, active: "opencode", ready: false,})const [themeSources, setThemeSources] = createSignal(allThemes())
subscribeThemes(setThemeSources)
const themeContext = createSimpleContext({ name: "Theme", init: (props: { mode: "dark" | "light"; source: ThemeSource }): ThemeContextValue => { const renderer = useRenderer() const configState = useConfig() const config = configState.data const themes = props.source const cache = useStorageOptional()?.store<{ colors?: TerminalColors }>("system-theme", { initial: {} })
setStore( produce((draft) => { const lock = config.theme?.mode === "dark" || config.theme?.mode === "light" ? config.theme.mode : undefined const mode = lock ?? renderer.themeMode ?? props.mode draft.mode = mode draft.lock = lock draft.active = config.theme?.name ?? "opencode" draft.ready = false }), )
createEffect(() => { const theme = config.theme?.name if (!theme) return setStore("active", theme) if (theme === "system") refreshPalette() })
createEffect(() => { const mode = config.theme?.mode if (mode === "dark" || mode === "light") { pin(mode, false) return } if (mode === "system" && store.lock !== undefined) free(false) })
function syncCustomThemes() { return themes .discover() .then((themes) => { setCustomThemes(themes) }) .catch(() => setStore("active", "opencode")) }
onMount(() => { // Terminal palette queries serialize with frame output. First paint uses the cached palette or built-in fallback. void syncCustomThemes().finally(() => { tokens() setStore("ready", true) }) })
const cachedPalette = cache?.[0].colors let palette = usablePalette(cachedPalette) ? cachedPalette : undefined const applyPalette = () => { if (palette) setSystemTheme(generateSystem(palette, store.mode)) } if (palette) { const mode = store.lock ?? terminalMode(palette) ?? store.mode if (store.mode !== mode) setStore("mode", mode) applyPalette() } else setSystemTheme(undefined)
let canProbe = false let probing = false let queued = false let disposed = false function refreshPalette() { if (store.active !== "system") return queued = true if (!canProbe || probing || disposed) return
queued = false probing = true // Clearing does not cancel a query already owned by the renderer. Share it, then run one fresh query. const retry = renderer.paletteDetectionStatus === "detecting" renderer.clearPaletteCache() void renderer .getPalette({ size: 16 }) .then((colors) => { if (disposed || !usablePalette(colors)) return palette = colors const mode = store.lock ?? terminalMode(colors) ?? store.mode if (store.mode !== mode) setStore("mode", mode) applyPalette() void cache?.[1]((draft) => { draft.colors = colors }).catch(() => {}) }) .catch(() => {}) .finally(() => { probing = false if (disposed || (!retry && !queued)) return refreshPalette() }) }
function afterPaint() { canProbe = true refreshPalette() }
function apply(mode: "dark" | "light") { if (store.mode !== mode) { setStore("mode", mode) applyPalette() } refreshPalette() }
function pin(mode: "dark" | "light" = store.mode, persist = true) { setStore("lock", mode) apply(mode) if (!persist) return void configState .update((draft) => { draft.theme = { ...draft.theme, mode } }) .catch(() => {}) }
function free(persist = true) { setStore("lock", undefined) apply(renderer.themeMode ?? store.mode) if (!persist) return void configState .update((draft) => { draft.theme = { ...draft.theme, mode: "system" } }) .catch(() => {}) }
const handleMode = (mode: "dark" | "light") => { if (store.lock) return apply(mode) } renderer.on(CliRenderEvents.THEME_MODE, handleMode)
const handleThemeNotification = (sequence: string) => { if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n") return false queueMicrotask(refreshPalette) return false } renderer.prependInputHandler(handleThemeNotification)
let themeRefreshTimeouts: ReturnType<typeof setTimeout>[] = [] const refreshThemes = () => { for (const timeout of themeRefreshTimeouts) clearTimeout(timeout) themeRefreshTimeouts = THEME_REFRESH_DELAYS.map((delay) => setTimeout(() => { refreshPalette() if (delay === THEME_REFRESH_DELAYS[THEME_REFRESH_DELAYS.length - 1]) void syncCustomThemes() }, delay), ) } const unsubscribeRefresh = themes.subscribeRefresh?.(refreshThemes)
onCleanup(() => { disposed = true renderer.off(CliRenderEvents.THEME_MODE, handleMode) renderer.removeInputHandler(handleThemeNotification) unsubscribeRefresh?.() for (const timeout of themeRefreshTimeouts) clearTimeout(timeout) themeRefreshTimeouts.length = 0 })
const initStarted = performance.now() const selected = createMemo(() => { const sources = themeSources() const name = sources[store.active] ? store.active : "opencode" try { return loadTheme(sources[name], name, store.mode) } catch (error) { if (name === "opencode") throw error themeErrors.emit(name, error) setStore("active", "opencode") return loadTheme(sources.opencode, "opencode", store.mode) } }) const modes = () => selected().modes const mode = () => selected().mode const tokens = () => selected().theme tokens() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) const current = createComponentTheme(tokens)
createEffect(() => renderer.setBackgroundColor(tokens().background.base))
const currentSyntax = createSyntaxStyleMemo(() => generateSyntax(tokens())) const service: Themes = { current, currentTokens: tokens, currentSyntax, get selected() { return store.active }, all: allThemes, has: hasTheme, mode, modes, supports: (requested) => modes().includes(requested), locked: () => store.lock !== undefined, lock: () => pin(mode()), unlock: free, setMode(requested = mode(), persist = true) { if (!modes().includes(requested)) return false pin(requested, persist) return true }, set(theme: string) { if (!hasTheme(theme)) return false setStore("active", theme) if (theme === "system") refreshPalette() void configState .update((draft) => { draft.theme = { ...draft.theme, name: theme } }) .catch(() => {}) return true }, afterPaint, onError: themeErrors.onError, get ready() { return store.ready }, } return { current, themes: service, get ready() { return service.ready }, } },})
export function useThemes() { return themeContext.use().themes}export function useTheme(): ComponentTheme { return themeContext.use().current}export const ThemeProvider = themeContext.provider
/** Switches the ambient theme surface without remounting children; undefined inherits the enclosing view. */export function ThemeContextProvider(props: ParentProps<{ context: SurfaceName | undefined }>) { const value = themeContext.use() const current = createComponentTheme(() => { const name = props.context return name ? value.themes.currentTokens().surface(name) : value.current }) return ( <themeContext.context.Provider value={{ current, themes: value.themes, ready: value.ready }}> {props.children} </themeContext.context.Provider> )}
function usablePalette(colors: TerminalColors | undefined): colors is TerminalColors { return Boolean( colors && (colors.defaultBackground ?? colors.palette[0]) && (colors.defaultForeground ?? colors.palette[7]), )}
function loadTheme(source: ThemeDocumentSource, name: string, requested: "dark" | "light") { const document = parseTheme(source, name) const modes = themeModes(document) const mode = modes.includes(requested) ? requested : (modes[0] ?? requested) return { modes, mode, theme: resolveThemeDocument(document, mode) }}
export function createSyntaxStyleMemo(factory: () => SyntaxStyle) { const renderer = useRenderer() const retained = new Set<SyntaxStyle>() let current: SyntaxStyle | undefined
const release = (style: SyntaxStyle) => { retained.add(style) void renderer .idle() .catch(() => {}) .finally(() => { if (!retained.delete(style)) return style.destroy() }) }
onCleanup(() => { if (current) release(current) })
return createMemo(() => { const previous = current current = factory() if (previous) release(previous) return current })}