tui plugins (#19347)
This commit is contained in:
parent
d8ad8338f5
commit
6274b0677c
91 changed files with 10544 additions and 898 deletions
|
|
@ -42,66 +42,13 @@ import { createStore, produce } from "solid-js/store"
|
|||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { useTuiConfig } from "./tui-config"
|
||||
import { isRecord } from "@/util/record"
|
||||
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
||||
|
||||
type ThemeColors = {
|
||||
primary: RGBA
|
||||
secondary: RGBA
|
||||
accent: RGBA
|
||||
error: RGBA
|
||||
warning: RGBA
|
||||
success: RGBA
|
||||
info: RGBA
|
||||
text: RGBA
|
||||
textMuted: RGBA
|
||||
selectedListItemText: RGBA
|
||||
background: RGBA
|
||||
backgroundPanel: RGBA
|
||||
backgroundElement: RGBA
|
||||
backgroundMenu: RGBA
|
||||
border: RGBA
|
||||
borderActive: RGBA
|
||||
borderSubtle: RGBA
|
||||
diffAdded: RGBA
|
||||
diffRemoved: RGBA
|
||||
diffContext: RGBA
|
||||
diffHunkHeader: RGBA
|
||||
diffHighlightAdded: RGBA
|
||||
diffHighlightRemoved: RGBA
|
||||
diffAddedBg: RGBA
|
||||
diffRemovedBg: RGBA
|
||||
diffContextBg: RGBA
|
||||
diffLineNumber: RGBA
|
||||
diffAddedLineNumberBg: RGBA
|
||||
diffRemovedLineNumberBg: RGBA
|
||||
markdownText: RGBA
|
||||
markdownHeading: RGBA
|
||||
markdownLink: RGBA
|
||||
markdownLinkText: RGBA
|
||||
markdownCode: RGBA
|
||||
markdownBlockQuote: RGBA
|
||||
markdownEmph: RGBA
|
||||
markdownStrong: RGBA
|
||||
markdownHorizontalRule: RGBA
|
||||
markdownListItem: RGBA
|
||||
markdownListEnumeration: RGBA
|
||||
markdownImage: RGBA
|
||||
markdownImageText: RGBA
|
||||
markdownCodeBlock: RGBA
|
||||
syntaxComment: RGBA
|
||||
syntaxKeyword: RGBA
|
||||
syntaxFunction: RGBA
|
||||
syntaxVariable: RGBA
|
||||
syntaxString: RGBA
|
||||
syntaxNumber: RGBA
|
||||
syntaxType: RGBA
|
||||
syntaxOperator: RGBA
|
||||
syntaxPunctuation: RGBA
|
||||
}
|
||||
|
||||
type Theme = ThemeColors & {
|
||||
type Theme = TuiThemeCurrent & {
|
||||
_hasSelectedListItemText: boolean
|
||||
thinkingOpacity: number
|
||||
}
|
||||
type ThemeColor = Exclude<keyof TuiThemeCurrent, "thinkingOpacity">
|
||||
|
||||
export function selectedForeground(theme: Theme, bg?: RGBA): RGBA {
|
||||
// If theme explicitly defines selectedListItemText, use it
|
||||
|
|
@ -128,10 +75,10 @@ type Variant = {
|
|||
light: HexColor | RefName
|
||||
}
|
||||
type ColorValue = HexColor | RefName | Variant | RGBA
|
||||
type ThemeJson = {
|
||||
export type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
|
||||
theme: Omit<Record<ThemeColor, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
|
||||
selectedListItemText?: ColorValue
|
||||
backgroundMenu?: ColorValue
|
||||
thinkingOpacity?: number
|
||||
|
|
@ -174,27 +121,91 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
|||
carbonfox,
|
||||
}
|
||||
|
||||
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
type State = {
|
||||
themes: Record<string, ThemeJson>
|
||||
mode: "dark" | "light"
|
||||
lock: "dark" | "light" | undefined
|
||||
active: string
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
const pluginThemes: Record<string, ThemeJson> = {}
|
||||
let customThemes: Record<string, ThemeJson> = {}
|
||||
let systemTheme: ThemeJson | undefined
|
||||
|
||||
function listThemes() {
|
||||
// Priority: defaults < plugin installs < custom files < generated system.
|
||||
const themes = {
|
||||
...DEFAULT_THEMES,
|
||||
...pluginThemes,
|
||||
...customThemes,
|
||||
}
|
||||
if (!systemTheme) return themes
|
||||
return {
|
||||
...themes,
|
||||
system: systemTheme,
|
||||
}
|
||||
}
|
||||
|
||||
function syncThemes() {
|
||||
setStore("themes", listThemes())
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore<State>({
|
||||
themes: listThemes(),
|
||||
mode: "dark",
|
||||
lock: undefined,
|
||||
active: "opencode",
|
||||
ready: false,
|
||||
})
|
||||
|
||||
export function allThemes() {
|
||||
return store.themes
|
||||
}
|
||||
|
||||
function isTheme(theme: unknown): theme is ThemeJson {
|
||||
if (!isRecord(theme)) return false
|
||||
if (!isRecord(theme.theme)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function hasTheme(name: string) {
|
||||
if (!name) return false
|
||||
return allThemes()[name] !== undefined
|
||||
}
|
||||
|
||||
export function addTheme(name: string, theme: unknown) {
|
||||
if (!name) return false
|
||||
if (!isTheme(theme)) return false
|
||||
if (hasTheme(name)) return false
|
||||
pluginThemes[name] = theme
|
||||
syncThemes()
|
||||
return true
|
||||
}
|
||||
|
||||
export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
const defs = theme.defs ?? {}
|
||||
function resolveColor(c: ColorValue): RGBA {
|
||||
function resolveColor(c: ColorValue, chain: string[] = []): RGBA {
|
||||
if (c instanceof RGBA) return c
|
||||
if (typeof c === "string") {
|
||||
if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0)
|
||||
|
||||
if (c.startsWith("#")) return RGBA.fromHex(c)
|
||||
|
||||
if (defs[c] != null) {
|
||||
return resolveColor(defs[c])
|
||||
} else if (theme.theme[c as keyof ThemeColors] !== undefined) {
|
||||
return resolveColor(theme.theme[c as keyof ThemeColors]!)
|
||||
} else {
|
||||
if (chain.includes(c)) {
|
||||
throw new Error(`Circular color reference: ${[...chain, c].join(" -> ")}`)
|
||||
}
|
||||
|
||||
const next = defs[c] ?? theme.theme[c as ThemeColor]
|
||||
if (next === undefined) {
|
||||
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
||||
}
|
||||
return resolveColor(next, [...chain, c])
|
||||
}
|
||||
if (typeof c === "number") {
|
||||
return ansiToRgba(c)
|
||||
}
|
||||
return resolveColor(c[mode])
|
||||
return resolveColor(c[mode], chain)
|
||||
}
|
||||
|
||||
const resolved = Object.fromEntries(
|
||||
|
|
@ -203,7 +214,7 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
|||
.map(([key, value]) => {
|
||||
return [key, resolveColor(value as ColorValue)]
|
||||
}),
|
||||
) as Partial<ThemeColors>
|
||||
) as Partial<Record<ThemeColor, RGBA>>
|
||||
|
||||
// Handle selectedListItemText separately since it's optional
|
||||
const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined
|
||||
|
|
@ -287,14 +298,18 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
if (value === "dark" || value === "light") return value
|
||||
return
|
||||
}
|
||||
const lock = pick(kv.get("theme_mode_lock"))
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES,
|
||||
mode: lock ?? pick(kv.get("theme_mode", props.mode)) ?? props.mode,
|
||||
lock,
|
||||
active: (config.theme ?? kv.get("theme", "opencode")) as string,
|
||||
ready: false,
|
||||
})
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const lock = pick(kv.get("theme_mode_lock"))
|
||||
const mode = pick(kv.get("theme_mode", props.mode))
|
||||
draft.mode = lock ?? mode ?? props.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
|
||||
|
|
@ -302,52 +317,46 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
})
|
||||
|
||||
function init() {
|
||||
resolveSystemTheme(store.mode)
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
Object.assign(draft.themes, custom)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
setStore("active", "opencode")
|
||||
})
|
||||
.finally(() => {
|
||||
if (store.active !== "system") {
|
||||
setStore("ready", true)
|
||||
}
|
||||
})
|
||||
Promise.allSettled([
|
||||
resolveSystemTheme(store.mode),
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
customThemes = custom
|
||||
syncThemes()
|
||||
})
|
||||
.catch(() => {
|
||||
setStore("active", "opencode")
|
||||
}),
|
||||
]).finally(() => {
|
||||
setStore("ready", true)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(init)
|
||||
|
||||
function resolveSystemTheme(mode: "dark" | "light" = store.mode) {
|
||||
renderer
|
||||
return renderer
|
||||
.getPalette({
|
||||
size: 16,
|
||||
})
|
||||
.then((colors) => {
|
||||
.then((colors: TerminalColors) => {
|
||||
if (!colors.palette[0]) {
|
||||
systemTheme = undefined
|
||||
syncThemes()
|
||||
if (store.active === "system") {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.active = "opencode"
|
||||
draft.ready = true
|
||||
}),
|
||||
)
|
||||
setStore("active", "opencode")
|
||||
}
|
||||
return
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.themes.system = generateSystem(colors, mode)
|
||||
if (store.active === "system") {
|
||||
draft.ready = true
|
||||
}
|
||||
}),
|
||||
)
|
||||
systemTheme = generateSystem(colors, mode)
|
||||
syncThemes()
|
||||
})
|
||||
.catch(() => {
|
||||
systemTheme = undefined
|
||||
syncThemes()
|
||||
if (store.active === "system") {
|
||||
setStore("active", "opencode")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -377,8 +386,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
apply(mode)
|
||||
}
|
||||
renderer.on(CliRenderEvents.THEME_MODE, handle)
|
||||
|
||||
const refresh = () => {
|
||||
renderer.clearPaletteCache()
|
||||
init()
|
||||
}
|
||||
process.on("SIGUSR2", refresh)
|
||||
|
||||
onCleanup(() => {
|
||||
renderer.off(CliRenderEvents.THEME_MODE, handle)
|
||||
process.off("SIGUSR2", refresh)
|
||||
})
|
||||
|
||||
const values = createMemo(() => {
|
||||
|
|
@ -403,7 +420,10 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
return store.active
|
||||
},
|
||||
all() {
|
||||
return store.themes
|
||||
return allThemes()
|
||||
},
|
||||
has(name: string) {
|
||||
return hasTheme(name)
|
||||
},
|
||||
syntax,
|
||||
subtleSyntax,
|
||||
|
|
@ -423,8 +443,10 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
pin(mode)
|
||||
},
|
||||
set(theme: string) {
|
||||
if (!hasTheme(theme)) return false
|
||||
setStore("active", theme)
|
||||
kv.set("theme", theme)
|
||||
return true
|
||||
},
|
||||
get ready() {
|
||||
return store.ready
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue