theme api
parent
3f9603e7a6
commit
29aab3223c
|
|
@ -1,5 +1,4 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import mytheme from "../themes/mytheme.json" with { type: "json" }
|
||||
import { extend, useKeyboard, useTerminalDimensions, type RenderableConstructor } from "@opentui/solid"
|
||||
import { RGBA, VignetteEffect, type OptimizedBuffer, type RenderContext } from "@opentui/core"
|
||||
import { ThreeRenderable, THREE } from "@opentui/core/3d"
|
||||
|
|
@ -726,13 +725,12 @@ const reg = (api: TuiApi, input: ReturnType<typeof cfg>) => {
|
|||
])
|
||||
}
|
||||
|
||||
const themes = {
|
||||
"workspace-plugin-smoke": mytheme,
|
||||
}
|
||||
|
||||
const tui = async (input: TuiPluginInput, options?: Record<string, unknown>) => {
|
||||
if (options?.enabled === false) return
|
||||
|
||||
await input.api.theme.install("../themes/mytheme.json")
|
||||
input.api.theme.set("mytheme")
|
||||
|
||||
const value = cfg(options)
|
||||
const route = names(value)
|
||||
const fx = new VignetteEffect(value.vignette)
|
||||
|
|
@ -770,6 +768,5 @@ const tui = async (input: TuiPluginInput, options?: Record<string, unknown>) =>
|
|||
}
|
||||
|
||||
export default {
|
||||
themes,
|
||||
tui,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"theme": "workspace-plugin-smoke",
|
||||
"theme": "mytheme",
|
||||
"plugin": [
|
||||
[
|
||||
"./plugins/tui-smoke.tsx",
|
||||
|
|
|
|||
|
|
@ -395,6 +395,15 @@ function App() {
|
|||
get selected() {
|
||||
return t.selected
|
||||
},
|
||||
has(name) {
|
||||
return t.has(name)
|
||||
},
|
||||
set(name) {
|
||||
return t.set(name)
|
||||
},
|
||||
async install(_jsonPath) {
|
||||
throw new Error("theme.install is only available in plugin context")
|
||||
},
|
||||
mode() {
|
||||
return t.mode()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ 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"
|
||||
|
||||
type ThemeColors = {
|
||||
primary: RGBA
|
||||
|
|
@ -203,15 +204,25 @@ export function allThemes() {
|
|||
return store.themes
|
||||
}
|
||||
|
||||
export function registerThemes(themes: Record<string, unknown>) {
|
||||
const list = Object.entries(themes).filter((entry): entry is [string, ThemeJson] => {
|
||||
const theme = entry[1]
|
||||
if (!theme || typeof theme !== "object") return false
|
||||
if (!("theme" in theme)) return false
|
||||
return true
|
||||
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
|
||||
mergeThemes({
|
||||
[name]: theme,
|
||||
})
|
||||
if (!list.length) return
|
||||
mergeThemes(Object.fromEntries(list))
|
||||
return true
|
||||
}
|
||||
|
||||
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
|
|
@ -414,6 +425,9 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
all() {
|
||||
return allThemes()
|
||||
},
|
||||
has(name: string) {
|
||||
return hasTheme(name)
|
||||
},
|
||||
syntax,
|
||||
subtleSyntax,
|
||||
mode() {
|
||||
|
|
@ -424,8 +438,10 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
kv.set("theme_mode", mode)
|
||||
},
|
||||
set(theme: string) {
|
||||
if (!hasTheme(theme)) return false
|
||||
setStore("active", theme)
|
||||
kv.set("theme", theme)
|
||||
return true
|
||||
},
|
||||
get ready() {
|
||||
return store.ready
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import {
|
|||
type TuiSlotMap,
|
||||
type TuiSlots,
|
||||
type SlotMode,
|
||||
type TuiApi,
|
||||
} from "@opencode-ai/plugin/tui"
|
||||
import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
|
||||
import type { CliRenderer } from "@opentui/core"
|
||||
import "@opentui/solid/preload"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import { Config } from "@/config/config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
|
|
@ -16,7 +19,9 @@ import { Log } from "@/util/log"
|
|||
import { isRecord } from "@/util/record"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { resolvePluginTarget, uniqueModuleEntries } from "@/plugin/shared"
|
||||
import { registerThemes } from "./context/theme"
|
||||
import { addTheme, hasTheme } from "./context/theme"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
type SlotProps<K extends keyof TuiSlotMap> = {
|
||||
name: K
|
||||
|
|
@ -45,12 +50,6 @@ function getTuiSlotPlugin(value: unknown) {
|
|||
return value.slots
|
||||
}
|
||||
|
||||
function getThemes(value: unknown) {
|
||||
if (!isRecord(value) || !("themes" in value)) return
|
||||
if (!isRecord(value.themes)) return
|
||||
return value.themes
|
||||
}
|
||||
|
||||
function isTuiPlugin(value: unknown): value is TuiPluginFn<CliRenderer> {
|
||||
return typeof value === "function"
|
||||
}
|
||||
|
|
@ -61,6 +60,90 @@ function getTuiPlugin(value: unknown) {
|
|||
return value.tui
|
||||
}
|
||||
|
||||
function isTheme(value: unknown) {
|
||||
if (!isRecord(value)) return false
|
||||
if (!isRecord(value.theme)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function localThemeDir(file: string) {
|
||||
const dir = path.dirname(file)
|
||||
if (path.basename(dir) === ".opencode") return path.join(dir, "themes")
|
||||
return path.join(dir, ".opencode", "themes")
|
||||
}
|
||||
|
||||
function themeDir(meta?: TuiConfig.PluginMeta) {
|
||||
if (meta?.scope === "local") return localThemeDir(meta.source)
|
||||
return path.join(Global.Path.config, "themes")
|
||||
}
|
||||
|
||||
function pluginDir(spec: string, target: string) {
|
||||
if (spec.startsWith("file://")) return path.dirname(fileURLToPath(spec))
|
||||
if (target.startsWith("file://")) return path.dirname(fileURLToPath(target))
|
||||
return target
|
||||
}
|
||||
|
||||
function themePath(root: string, filepath: string) {
|
||||
if (filepath.startsWith("file://")) return fileURLToPath(filepath)
|
||||
if (path.isAbsolute(filepath)) return filepath
|
||||
return path.resolve(root, filepath)
|
||||
}
|
||||
|
||||
function themeName(filepath: string) {
|
||||
return path.basename(filepath, path.extname(filepath))
|
||||
}
|
||||
|
||||
function themeApi(
|
||||
api: TuiApi<JSX.Element>,
|
||||
options: {
|
||||
root: string
|
||||
meta?: TuiConfig.PluginMeta
|
||||
},
|
||||
) {
|
||||
return {
|
||||
get current() {
|
||||
return api.theme.current
|
||||
},
|
||||
get selected() {
|
||||
return api.theme.selected
|
||||
},
|
||||
mode() {
|
||||
return api.theme.mode()
|
||||
},
|
||||
get ready() {
|
||||
return api.theme.ready
|
||||
},
|
||||
has(name: string) {
|
||||
return api.theme.has(name)
|
||||
},
|
||||
set(name: string) {
|
||||
return api.theme.set(name)
|
||||
},
|
||||
async install(filepath: string) {
|
||||
const source = themePath(options.root, filepath)
|
||||
const name = themeName(source)
|
||||
if (hasTheme(name)) return
|
||||
|
||||
const text = await Bun.file(source)
|
||||
.text()
|
||||
.catch((error) => {
|
||||
throw new Error(`failed to read theme at ${source}: ${error}`)
|
||||
})
|
||||
const data = JSON.parse(text)
|
||||
if (!isTheme(data)) {
|
||||
throw new Error(`invalid theme at ${source}`)
|
||||
}
|
||||
|
||||
const dest = path.join(themeDir(options.meta), `${name}.json`)
|
||||
if (!(await Filesystem.exists(dest))) {
|
||||
await Filesystem.write(dest, text)
|
||||
}
|
||||
|
||||
addTheme(name, data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TuiPlugin {
|
||||
const log = Log.create({ service: "tui.plugin" })
|
||||
let loaded: Promise<void> | undefined
|
||||
|
|
@ -128,6 +211,7 @@ export namespace TuiPlugin {
|
|||
|
||||
const loadOne = async (item: (typeof plugins)[number], retry = false) => {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
const meta = config.plugin_meta?.[Config.getPluginName(item)]
|
||||
log.info("loading tui plugin", { path: spec, retry })
|
||||
const target = await resolvePluginTarget(spec).catch((error) => {
|
||||
log.error("failed to resolve tui plugin", { path: spec, retry, error })
|
||||
|
|
@ -151,15 +235,25 @@ export namespace TuiPlugin {
|
|||
continue
|
||||
}
|
||||
|
||||
const theme = getThemes(entry)
|
||||
if (theme) registerThemes(theme)
|
||||
|
||||
const slotPlugin = getTuiSlotPlugin(entry)
|
||||
if (slotPlugin) input.slots.register(slotPlugin)
|
||||
|
||||
const tuiPlugin = getTuiPlugin(entry)
|
||||
if (!tuiPlugin) continue
|
||||
await tuiPlugin(input, Config.pluginOptions(item))
|
||||
const root = pluginDir(spec, target)
|
||||
await tuiPlugin(
|
||||
{
|
||||
...input,
|
||||
api: {
|
||||
command: input.api.command,
|
||||
route: input.api.route,
|
||||
ui: input.api.ui,
|
||||
keybind: input.api.keybind,
|
||||
theme: themeApi(input.api, { root, meta }),
|
||||
},
|
||||
},
|
||||
Config.pluginOptions(item),
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|||
import type { CliRenderer } from "@opentui/core"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { Log } from "../../../src/util/log"
|
||||
import { Global } from "../../../src/global"
|
||||
|
||||
mock.module("@opentui/solid/preload", () => ({}))
|
||||
mock.module("@opentui/solid/jsx-runtime", () => ({
|
||||
|
|
@ -14,6 +15,7 @@ mock.module("@opentui/solid/jsx-runtime", () => ({
|
|||
jsxs: () => null,
|
||||
jsxDEV: () => null,
|
||||
}))
|
||||
const { allThemes } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
const { TuiPlugin } = await import("../../../src/cli/cmd/tui/plugin")
|
||||
|
||||
async function waitForLog(text: string, timeout = 1000) {
|
||||
|
|
@ -33,16 +35,35 @@ async function waitForLog(text: string, timeout = 1000) {
|
|||
.catch(() => "")
|
||||
}
|
||||
|
||||
test("ignores function-only tui exports and loads object exports", async () => {
|
||||
test("loads plugin theme API with scoped theme installation", async () => {
|
||||
const stamp = Date.now()
|
||||
const globalConfigPath = path.join(Global.Path.config, "tui.json")
|
||||
const backup = await Bun.file(globalConfigPath)
|
||||
.text()
|
||||
.catch(() => undefined)
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
const pluginPath = path.join(dir, "plugin.ts")
|
||||
const localPluginPath = path.join(dir, "local-plugin.ts")
|
||||
const globalPluginPath = path.join(dir, "global-plugin.ts")
|
||||
const localThemeFile = `local-theme-${stamp}.json`
|
||||
const globalThemeFile = `global-theme-${stamp}.json`
|
||||
const localThemeName = localThemeFile.replace(/\.json$/, "")
|
||||
const globalThemeName = globalThemeFile.replace(/\.json$/, "")
|
||||
const localThemePath = path.join(dir, localThemeFile)
|
||||
const globalThemePath = path.join(dir, globalThemeFile)
|
||||
const localDest = path.join(dir, ".opencode", "themes", localThemeFile)
|
||||
const globalDest = path.join(Global.Path.config, "themes", globalThemeFile)
|
||||
const fnMarker = path.join(dir, "function-called.txt")
|
||||
const objMarker = path.join(dir, "object-called.txt")
|
||||
const configPath = path.join(dir, "tui.json")
|
||||
const localMarker = path.join(dir, "local-called.json")
|
||||
const globalMarker = path.join(dir, "global-called.json")
|
||||
const localConfigPath = path.join(dir, "tui.json")
|
||||
|
||||
await Bun.write(localThemePath, JSON.stringify({ theme: { primary: "#101010" } }, null, 2))
|
||||
await Bun.write(globalThemePath, JSON.stringify({ theme: { primary: "#202020" } }, null, 2))
|
||||
|
||||
await Bun.write(
|
||||
pluginPath,
|
||||
localPluginPath,
|
||||
[
|
||||
"export default async (_input, options) => {",
|
||||
" if (!options?.fn_marker) return",
|
||||
|
|
@ -50,9 +71,21 @@ test("ignores function-only tui exports and loads object exports", async () => {
|
|||
"}",
|
||||
"",
|
||||
"export const object_plugin = {",
|
||||
" tui: async (_input, options) => {",
|
||||
" if (!options?.obj_marker) return",
|
||||
" await Bun.write(options.obj_marker, 'called')",
|
||||
" tui: async (input, options) => {",
|
||||
" if (!options?.marker) return",
|
||||
" const before = input.api.theme.has(options.theme_name)",
|
||||
" const set_missing = input.api.theme.set(options.theme_name)",
|
||||
" await input.api.theme.install(options.theme_path)",
|
||||
" const after = input.api.theme.has(options.theme_name)",
|
||||
" const set_installed = input.api.theme.set(options.theme_name)",
|
||||
" const first = await Bun.file(options.dest).text()",
|
||||
" await Bun.write(options.source, JSON.stringify({ theme: { primary: '#fefefe' } }, null, 2))",
|
||||
" await input.api.theme.install(options.theme_path)",
|
||||
" const second = await Bun.file(options.dest).text()",
|
||||
" await Bun.write(",
|
||||
" options.marker,",
|
||||
" JSON.stringify({ before, set_missing, after, set_installed, selected: input.api.theme.selected, same: first === second }),",
|
||||
" )",
|
||||
" },",
|
||||
"}",
|
||||
"",
|
||||
|
|
@ -60,10 +93,54 @@ test("ignores function-only tui exports and loads object exports", async () => {
|
|||
)
|
||||
|
||||
await Bun.write(
|
||||
configPath,
|
||||
globalPluginPath,
|
||||
[
|
||||
"export default {",
|
||||
" tui: async (input, options) => {",
|
||||
" if (!options?.marker) return",
|
||||
" await input.api.theme.install(options.theme_path)",
|
||||
" const has = input.api.theme.has(options.theme_name)",
|
||||
" const set_installed = input.api.theme.set(options.theme_name)",
|
||||
" await Bun.write(options.marker, JSON.stringify({ has, set_installed, selected: input.api.theme.selected }))",
|
||||
" },",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
globalConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
plugin: [[pathToFileURL(pluginPath).href, { fn_marker: fnMarker, obj_marker: objMarker }]],
|
||||
plugin: [
|
||||
[
|
||||
pathToFileURL(globalPluginPath).href,
|
||||
{ marker: globalMarker, theme_path: `./${globalThemeFile}`, theme_name: globalThemeName },
|
||||
],
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
localConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
plugin: [
|
||||
[
|
||||
pathToFileURL(localPluginPath).href,
|
||||
{
|
||||
fn_marker: fnMarker,
|
||||
marker: localMarker,
|
||||
source: localThemePath,
|
||||
dest: localDest,
|
||||
theme_path: `./${localThemeFile}`,
|
||||
theme_name: localThemeName,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
|
@ -71,15 +148,21 @@ test("ignores function-only tui exports and loads object exports", async () => {
|
|||
)
|
||||
|
||||
return {
|
||||
configPath,
|
||||
localThemeFile,
|
||||
globalThemeFile,
|
||||
localThemeName,
|
||||
globalThemeName,
|
||||
localDest,
|
||||
globalDest,
|
||||
fnMarker,
|
||||
objMarker,
|
||||
localMarker,
|
||||
globalMarker,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
process.env.OPENCODE_TUI_CONFIG = tmp.extra.configPath
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
let selected = "opencode"
|
||||
|
||||
const renderer = {
|
||||
...Object.create(null),
|
||||
|
|
@ -133,7 +216,18 @@ test("ignores function-only tui exports and loads object exports", async () => {
|
|||
return {}
|
||||
},
|
||||
get selected() {
|
||||
return "opencode"
|
||||
return selected
|
||||
},
|
||||
has(name) {
|
||||
return allThemes()[name] !== undefined
|
||||
},
|
||||
set(name) {
|
||||
if (!allThemes()[name]) return false
|
||||
selected = name
|
||||
return true
|
||||
},
|
||||
async install() {
|
||||
throw new Error("base theme.install should not run")
|
||||
},
|
||||
mode() {
|
||||
return "dark" as const
|
||||
|
|
@ -145,15 +239,52 @@ test("ignores function-only tui exports and loads object exports", async () => {
|
|||
},
|
||||
})
|
||||
|
||||
expect(await fs.readFile(tmp.extra.objMarker, "utf8")).toBe("called")
|
||||
const local = JSON.parse(await fs.readFile(tmp.extra.localMarker, "utf8"))
|
||||
expect(local.before).toBe(false)
|
||||
expect(local.set_missing).toBe(false)
|
||||
expect(local.after).toBe(true)
|
||||
expect(local.set_installed).toBe(true)
|
||||
expect(local.selected).toBe(tmp.extra.localThemeName)
|
||||
expect(local.same).toBe(true)
|
||||
|
||||
const global = JSON.parse(await fs.readFile(tmp.extra.globalMarker, "utf8"))
|
||||
expect(global.has).toBe(true)
|
||||
expect(global.set_installed).toBe(true)
|
||||
expect(global.selected).toBe(tmp.extra.globalThemeName)
|
||||
|
||||
await expect(fs.readFile(tmp.extra.fnMarker, "utf8")).rejects.toThrow()
|
||||
|
||||
const localInstalled = await fs.readFile(tmp.extra.localDest, "utf8")
|
||||
expect(localInstalled).toContain("#101010")
|
||||
expect(localInstalled).not.toContain("#fefefe")
|
||||
|
||||
const globalInstalled = await fs.readFile(tmp.extra.globalDest, "utf8")
|
||||
expect(globalInstalled).toContain("#202020")
|
||||
|
||||
expect(
|
||||
await fs
|
||||
.stat(path.join(Global.Path.config, "themes", tmp.extra.localThemeFile))
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
).toBe(false)
|
||||
expect(
|
||||
await fs
|
||||
.stat(path.join(tmp.path, ".opencode", "themes", tmp.extra.globalThemeFile))
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
).toBe(false)
|
||||
|
||||
const log = await waitForLog("ignoring non-object tui plugin export")
|
||||
expect(log).toContain("ignoring non-object tui plugin export")
|
||||
expect(log).toContain("name=default")
|
||||
expect(log).toContain("type=function")
|
||||
} finally {
|
||||
cwd.mockRestore()
|
||||
delete process.env.OPENCODE_TUI_CONFIG
|
||||
if (backup === undefined) {
|
||||
await fs.rm(globalConfigPath, { force: true })
|
||||
} else {
|
||||
await Bun.write(globalConfigPath, backup)
|
||||
}
|
||||
await fs.rm(tmp.extra.globalDest, { force: true }).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,33 +7,38 @@ mock.module("@opentui/solid/jsx-runtime", () => ({
|
|||
jsxDEV: () => null,
|
||||
}))
|
||||
|
||||
const { DEFAULT_THEMES, allThemes, registerThemes } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
const { DEFAULT_THEMES, allThemes, addTheme, hasTheme } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
|
||||
test("registerThemes writes into module theme store", () => {
|
||||
test("addTheme writes into module theme store", () => {
|
||||
const name = `plugin-theme-${Date.now()}`
|
||||
registerThemes({
|
||||
[name]: DEFAULT_THEMES.opencode,
|
||||
})
|
||||
expect(addTheme(name, DEFAULT_THEMES.opencode)).toBe(true)
|
||||
|
||||
expect(allThemes()[name]).toBeDefined()
|
||||
})
|
||||
|
||||
test("registerThemes keeps first theme for duplicate names", () => {
|
||||
test("addTheme keeps first theme for duplicate names", () => {
|
||||
const name = `plugin-theme-keep-${Date.now()}`
|
||||
const one = structuredClone(DEFAULT_THEMES.opencode)
|
||||
const two = structuredClone(DEFAULT_THEMES.opencode)
|
||||
;(one.theme as Record<string, unknown>).primary = "#101010"
|
||||
;(two.theme as Record<string, unknown>).primary = "#fefefe"
|
||||
|
||||
registerThemes({ [name]: one })
|
||||
registerThemes({ [name]: two })
|
||||
expect(addTheme(name, one)).toBe(true)
|
||||
expect(addTheme(name, two)).toBe(false)
|
||||
|
||||
expect(allThemes()[name]).toBeDefined()
|
||||
expect(allThemes()[name]!.theme.primary).toBe("#101010")
|
||||
})
|
||||
|
||||
test("registerThemes ignores entries without a theme object", () => {
|
||||
test("addTheme ignores entries without a theme object", () => {
|
||||
const name = `plugin-theme-invalid-${Date.now()}`
|
||||
registerThemes({ [name]: { defs: { a: "#ffffff" } } })
|
||||
expect(addTheme(name, { defs: { a: "#ffffff" } })).toBe(false)
|
||||
expect(allThemes()[name]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("hasTheme checks theme presence", () => {
|
||||
const name = `plugin-theme-has-${Date.now()}`
|
||||
expect(hasTheme(name)).toBe(false)
|
||||
expect(addTheme(name, DEFAULT_THEMES.opencode)).toBe(true)
|
||||
expect(hasTheme(name)).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,24 +4,6 @@ import type { Plugin as ServerPlugin, PluginOptions } from "./index"
|
|||
|
||||
export type { CliRenderer, SlotMode } from "@opentui/core"
|
||||
|
||||
type HexColor = `#${string}`
|
||||
type RefName = string
|
||||
type Variant = {
|
||||
dark: HexColor | RefName | number
|
||||
light: HexColor | RefName | number
|
||||
}
|
||||
type ThemeColorValue = HexColor | RefName | number | Variant
|
||||
|
||||
export type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Record<string, ThemeColorValue> & {
|
||||
selectedListItemText?: ThemeColorValue
|
||||
backgroundMenu?: ThemeColorValue
|
||||
thinkingOpacity?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type TuiRouteCurrent =
|
||||
| {
|
||||
name: "home"
|
||||
|
|
@ -128,6 +110,9 @@ export type TuiToast = {
|
|||
export type TuiTheme = {
|
||||
readonly current: Record<string, unknown>
|
||||
readonly selected: string
|
||||
has: (name: string) => boolean
|
||||
set: (name: string) => boolean
|
||||
install: (jsonPath: string) => Promise<void>
|
||||
mode: () => "dark" | "light"
|
||||
readonly ready: boolean
|
||||
}
|
||||
|
|
@ -200,5 +185,4 @@ export type TuiPluginModule<Renderer = CliRenderer, Node = unknown> = {
|
|||
server?: ServerPlugin
|
||||
tui?: TuiPlugin<Renderer, Node>
|
||||
slots?: TuiSlotPlugin
|
||||
themes?: Record<string, ThemeJson>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue