Buckets:
| import { CliRenderEvents, SyntaxStyle, type TerminalColors } from "@opentui/core" | |
| import { useRenderer } from "@opentui/solid" | |
| import { | |
| DEFAULT_THEMES, | |
| addTheme, | |
| allThemes, | |
| generateSubtleSyntax, | |
| generateSyntax, | |
| generateSystem, | |
| hasTheme, | |
| isTheme, | |
| resolveTheme, | |
| selectedForeground, | |
| setCustomThemes, | |
| setSystemTheme, | |
| subscribeThemes, | |
| terminalMode, | |
| tint, | |
| upsertTheme, | |
| type ThemeJson, | |
| } from "../theme" | |
| import { createEffect, createMemo, onCleanup, onMount } from "solid-js" | |
| import { createStore, produce } from "solid-js/store" | |
| import { createSimpleContext } from "./helper" | |
| import { useKV } from "./kv" | |
| import { useTuiConfig } from "../config" | |
| import { Global } from "@opencode-ai/core/global" | |
| import { Glob } from "@opencode-ai/core/util/glob" | |
| import { readFile } from "node:fs/promises" | |
| import path from "node:path" | |
| export type ThemeSource = Readonly<{ | |
| discover(): Promise<Record<string, unknown>> | |
| subscribeRefresh?(refresh: () => void): () => void | |
| }> | |
| const themeSource: ThemeSource = { | |
| async discover() { | |
| const directories = [Global.Path.config] | |
| for (let current = process.cwd(); ; current = path.dirname(current)) { | |
| directories.push(path.join(current, ".opencode")) | |
| if (path.dirname(current) === current) break | |
| } | |
| return discoverThemes(directories) | |
| }, | |
| subscribeRefresh(refresh) { | |
| process.on("SIGUSR2", refresh) | |
| return () => process.off("SIGUSR2", refresh) | |
| }, | |
| } | |
| export async function discoverThemes(directories: string[]) { | |
| const result: Record<string, unknown> = {} | |
| for (const directory of directories) { | |
| const files = await Glob.scan("themes/*.json", { cwd: directory, absolute: true, dot: true, symlink: true }) | |
| for (const file of files) { | |
| result[path.basename(file, ".json")] = JSON.parse(await readFile(file, "utf8")) as unknown | |
| } | |
| } | |
| return result | |
| } | |
| export { | |
| DEFAULT_THEMES, | |
| addTheme, | |
| allThemes, | |
| generateSubtleSyntax, | |
| generateSyntax, | |
| generateSystem, | |
| hasTheme, | |
| isTheme, | |
| resolveTheme, | |
| selectedForeground, | |
| terminalMode, | |
| tint, | |
| upsertTheme, | |
| type Theme, | |
| type ThemeJson, | |
| type SyntaxStyleOverrides, | |
| } from "../theme" | |
| const THEME_REFRESH_DELAYS = [250, 1000] as const | |
| type State = { | |
| themes: Record<string, ThemeJson> | |
| mode: "dark" | "light" | |
| lock: "dark" | "light" | undefined | |
| active: string | |
| ready: boolean | |
| } | |
| const [store, setStore] = createStore<State>({ | |
| themes: allThemes(), | |
| mode: "dark", | |
| lock: undefined, | |
| active: "opencode", | |
| ready: false, | |
| }) | |
| subscribeThemes((themes) => setStore("themes", themes)) | |
| export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ | |
| name: "Theme", | |
| init: (props: { mode: "dark" | "light"; source?: ThemeSource }) => { | |
| const renderer = useRenderer() | |
| const config = useTuiConfig() | |
| const kv = useKV() | |
| const themes = props.source ?? themeSource | |
| const pick = (value: unknown) => { | |
| if (value === "dark" || value === "light") return value | |
| return | |
| } | |
| setStore( | |
| produce((draft) => { | |
| const lock = pick(kv.get("theme_mode_lock")) | |
| const mode = lock ?? pick(renderer.themeMode) ?? props.mode | |
| if (!lock && pick(kv.get("theme_mode")) !== undefined) kv.set("theme_mode", undefined) | |
| draft.mode = mode | |
| draft.lock = lock | |
| const active = config.theme ?? kv.get("theme", "opencode") | |
| draft.active = typeof active === "string" ? active : "opencode" | |
| draft.ready = false | |
| }), | |
| ) | |
| createEffect(() => { | |
| const theme = config.theme | |
| if (theme) setStore("active", theme) | |
| }) | |
| function syncCustomThemes() { | |
| return themes | |
| .discover() | |
| .then((themes) => { | |
| setCustomThemes( | |
| Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => { | |
| if (isTheme(theme)) result[name] = theme | |
| return result | |
| }, {}), | |
| ) | |
| }) | |
| .catch(() => setStore("active", "opencode")) | |
| } | |
| onMount(() => { | |
| void Promise.allSettled([resolveSystemTheme(store.mode), syncCustomThemes()]).finally(() => { | |
| setStore("ready", true) | |
| }) | |
| }) | |
| let systemThemeSignature: string | undefined | |
| let systemThemeMode: "dark" | "light" | undefined | |
| let hasResolvedSystemTheme = false | |
| function resolveSystemTheme(mode: "dark" | "light" = store.mode) { | |
| return renderer | |
| .getPalette({ size: 16 }) | |
| .then((colors: TerminalColors) => { | |
| if (!colors.palette[0]) { | |
| if (hasResolvedSystemTheme) return | |
| setSystemTheme(undefined) | |
| if (store.active === "system") setStore("active", "opencode") | |
| return | |
| } | |
| const next = store.lock ?? terminalMode(colors) ?? mode | |
| if (store.mode !== next) setStore("mode", next) | |
| const signature = JSON.stringify(colors) | |
| hasResolvedSystemTheme = true | |
| if (store.themes.system && systemThemeSignature === signature && systemThemeMode === next) return | |
| systemThemeSignature = signature | |
| systemThemeMode = next | |
| setSystemTheme(generateSystem(colors, next)) | |
| }) | |
| .catch(() => { | |
| if (hasResolvedSystemTheme) return | |
| setSystemTheme(undefined) | |
| if (store.active === "system") setStore("active", "opencode") | |
| }) | |
| } | |
| let systemRefreshRunning = false | |
| let systemRefreshQueued = false | |
| let systemRefreshMode = store.mode | |
| function refreshSystemTheme(mode: "dark" | "light" = store.mode) { | |
| systemRefreshMode = mode | |
| if (systemRefreshRunning) { | |
| systemRefreshQueued = true | |
| return | |
| } | |
| systemRefreshRunning = true | |
| const retry = renderer.paletteDetectionStatus === "detecting" | |
| renderer.clearPaletteCache() | |
| void resolveSystemTheme(mode).finally(() => { | |
| systemRefreshRunning = false | |
| if (!retry && !systemRefreshQueued) return | |
| systemRefreshQueued = false | |
| refreshSystemTheme(systemRefreshMode) | |
| }) | |
| } | |
| function apply(mode: "dark" | "light") { | |
| if (store.lock !== undefined) kv.set("theme_mode", mode) | |
| if (store.mode === mode) return | |
| setStore("mode", mode) | |
| refreshSystemTheme(mode) | |
| } | |
| function pin(mode: "dark" | "light" = store.mode) { | |
| setStore("lock", mode) | |
| kv.set("theme_mode_lock", mode) | |
| apply(mode) | |
| } | |
| function free() { | |
| setStore("lock", undefined) | |
| kv.set("theme_mode_lock", undefined) | |
| kv.set("theme_mode", undefined) | |
| refreshSystemTheme(renderer.themeMode ?? store.mode) | |
| } | |
| const handle = (mode: "dark" | "light") => { | |
| if (store.lock) return | |
| apply(mode) | |
| } | |
| renderer.on(CliRenderEvents.THEME_MODE, handle) | |
| const handleThemeNotification = (sequence: string) => { | |
| if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n") return false | |
| queueMicrotask(() => refreshSystemTheme()) | |
| return false | |
| } | |
| renderer.prependInputHandler(handleThemeNotification) | |
| let themeRefreshTimeouts: ReturnType<typeof setTimeout>[] = [] | |
| const refresh = () => { | |
| for (const timeout of themeRefreshTimeouts) clearTimeout(timeout) | |
| themeRefreshTimeouts = THEME_REFRESH_DELAYS.map((delay) => | |
| setTimeout(() => { | |
| refreshSystemTheme() | |
| if (delay === THEME_REFRESH_DELAYS[THEME_REFRESH_DELAYS.length - 1]) void syncCustomThemes() | |
| }, delay), | |
| ) | |
| } | |
| let unsubscribeRefresh: (() => void) | undefined | |
| unsubscribeRefresh = themes.subscribeRefresh?.(refresh) | |
| onCleanup(() => { | |
| renderer.off(CliRenderEvents.THEME_MODE, handle) | |
| renderer.removeInputHandler(handleThemeNotification) | |
| unsubscribeRefresh?.() | |
| for (const timeout of themeRefreshTimeouts) clearTimeout(timeout) | |
| themeRefreshTimeouts.length = 0 | |
| }) | |
| const values = createMemo(() => { | |
| const active = store.themes[store.active] | |
| if (active) return resolveTheme(active, store.mode) | |
| const saved = kv.get("theme") | |
| if (typeof saved === "string") { | |
| const theme = store.themes[saved] | |
| if (theme) return resolveTheme(theme, store.mode) | |
| } | |
| return resolveTheme(store.themes.opencode, store.mode) | |
| }) | |
| createEffect(() => renderer.setBackgroundColor(values().background)) | |
| const syntax = createSyntaxStyleMemo(() => generateSyntax(values())) | |
| const subtleSyntax = createSyntaxStyleMemo(() => generateSubtleSyntax(values())) | |
| return { | |
| theme: new Proxy(values(), { | |
| get(_target, prop) { | |
| // @ts-expect-error Properties are forwarded to the current reactive value. | |
| return values()[prop] | |
| }, | |
| }), | |
| get selected() { | |
| return store.active | |
| }, | |
| all: allThemes, | |
| has: hasTheme, | |
| syntax, | |
| subtleSyntax, | |
| mode: () => store.mode, | |
| locked: () => store.lock !== undefined, | |
| lock: () => pin(store.mode), | |
| unlock: free, | |
| setMode: pin, | |
| set(theme: string) { | |
| if (!hasTheme(theme)) return false | |
| setStore("active", theme) | |
| kv.set("theme", theme) | |
| return true | |
| }, | |
| get ready() { | |
| return store.ready | |
| }, | |
| } | |
| }, | |
| }) | |
| 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 | |
| }) | |
| } | |
Xet Storage Details
- Size:
- 9.86 kB
- Xet hash:
- 188b1873cab61e01a44196f104cc6a67a361a7c8fece35f5506979e22c38dec4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.