From f9385bcc631d8f3ff133a234c9ac2cee6259c67f Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Fri, 6 Mar 2026 22:55:28 +0100 Subject: [PATCH] cleanup --- .../mytheme.json => plugins/smoke-theme.json} | 0 .opencode/plugins/tui-smoke.tsx | 4 +- .opencode/themes/.gitignore | 1 + .opencode/tui.json | 2 +- packages/opencode/src/cli/cmd/tui/plugin.ts | 119 +++++++++--------- .../test/cli/tui/plugin-loader.test.ts | 72 +++++------ 6 files changed, 95 insertions(+), 103 deletions(-) rename .opencode/{themes/mytheme.json => plugins/smoke-theme.json} (100%) create mode 100644 .opencode/themes/.gitignore diff --git a/.opencode/themes/mytheme.json b/.opencode/plugins/smoke-theme.json similarity index 100% rename from .opencode/themes/mytheme.json rename to .opencode/plugins/smoke-theme.json diff --git a/.opencode/plugins/tui-smoke.tsx b/.opencode/plugins/tui-smoke.tsx index 2cfb84fcda..c8e71103cb 100644 --- a/.opencode/plugins/tui-smoke.tsx +++ b/.opencode/plugins/tui-smoke.tsx @@ -728,8 +728,8 @@ const reg = (api: TuiApi, input: ReturnType) => { const tui = async (input: TuiPluginInput, options?: Record) => { if (options?.enabled === false) return - await input.api.theme.install("../themes/mytheme.json") - input.api.theme.set("mytheme") + await input.api.theme.install("./smoke-theme.json") + input.api.theme.set("smoke-theme") const value = cfg(options) const route = names(value) diff --git a/.opencode/themes/.gitignore b/.opencode/themes/.gitignore new file mode 100644 index 0000000000..5b41319c6a --- /dev/null +++ b/.opencode/themes/.gitignore @@ -0,0 +1 @@ +smoke-theme.json diff --git a/.opencode/tui.json b/.opencode/tui.json index ada23dc578..c40a61ca2b 100644 --- a/.opencode/tui.json +++ b/.opencode/tui.json @@ -1,6 +1,6 @@ { "$schema": "https://opencode.ai/tui.json", - "theme": "mytheme", + "theme": "smoke-theme", "plugin": [ [ "./plugins/tui-smoke.tsx", diff --git a/packages/opencode/src/cli/cmd/tui/plugin.ts b/packages/opencode/src/cli/cmd/tui/plugin.ts index 8bc9f296d5..e5a23954ec 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin.ts @@ -1,11 +1,11 @@ import { type TuiPlugin as TuiPluginFn, type TuiPluginInput, + type TuiTheme, type TuiSlotContext, 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" @@ -66,82 +66,75 @@ function isTheme(value: unknown) { return true } -function localThemeDir(file: string) { +function localDir(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) +function scopeDir(meta: TuiConfig.PluginMeta) { + if (meta.scope === "local") return localDir(meta.source) return path.join(Global.Path.config, "themes") } -function pluginDir(spec: string, target: string) { +function pluginRoot(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 source(root: string, file: string) { + if (file.startsWith("file://")) return fileURLToPath(file) + if (path.isAbsolute(file)) return file + return path.resolve(root, file) } -function themeName(filepath: string) { - return path.basename(filepath, path.extname(filepath)) +function name(file: string) { + return path.basename(file, path.extname(file)) } -function themeApi( - api: TuiApi, - 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) - }, +function meta(config: TuiConfig.Info, item: Config.PluginSpec) { + const key = Config.getPluginName(item) + const value = config.plugin_meta?.[key] + if (!value) { + throw new Error(`missing plugin metadata for ${key}`) } + return value +} + +function install(meta: TuiConfig.PluginMeta, root: string): TuiTheme["install"] { + return async (file) => { + const src = source(root, file) + const theme = name(src) + if (hasTheme(theme)) return + + const text = await Bun.file(src) + .text() + .catch((error) => { + throw new Error(`failed to read theme at ${src}: ${error}`) + }) + const data = JSON.parse(text) + if (!isTheme(data)) { + throw new Error(`invalid theme at ${src}`) + } + + const dest = path.join(scopeDir(meta), `${theme}.json`) + if (!(await Filesystem.exists(dest))) { + await Filesystem.write(dest, text) + } + + addTheme(theme, data) + } +} + +function themeApi(theme: TuiTheme, add: TuiTheme["install"]): TuiTheme { + return Object.create(theme, { + install: { + value: add, + configurable: true, + enumerable: true, + }, + }) } export namespace TuiPlugin { @@ -211,7 +204,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)] + const level = meta(config, 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 }) @@ -219,6 +212,9 @@ export namespace TuiPlugin { }) if (!target) return false + const root = pluginRoot(spec, target) + const add = install(level, root) + const mod = await import(target).catch((error) => { log.error("failed to load tui plugin", { path: spec, retry, error }) return @@ -240,7 +236,6 @@ export namespace TuiPlugin { const tuiPlugin = getTuiPlugin(entry) if (!tuiPlugin) continue - const root = pluginDir(spec, target) await tuiPlugin( { ...input, @@ -249,7 +244,7 @@ export namespace TuiPlugin { route: input.api.route, ui: input.api.ui, keybind: input.api.keybind, - theme: themeApi(input.api, { root, meta }), + theme: themeApi(input.api.theme, add), }, }, Config.pluginOptions(item), diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index ab6e046abe..d34752c31f 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -64,48 +64,44 @@ test("loads plugin theme API with scoped theme installation", async () => { await Bun.write( localPluginPath, - [ - "export default async (_input, options) => {", - " if (!options?.fn_marker) return", - " await Bun.write(options.fn_marker, 'called')", - "}", - "", - "export const object_plugin = {", - " 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 }),", - " )", - " },", - "}", - "", - ].join("\n"), + `export default async (_input, options) => { + if (!options?.fn_marker) return + await Bun.write(options.fn_marker, "called") +} + +export const object_plugin = { + 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 }), + ) + }, +} +`, ) await Bun.write( 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"), + `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 })) + }, +} +`, ) await Bun.write(