summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/scripts
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-03 09:04:28 -0500
committerAdam <[email protected]>2025-10-03 09:04:28 -0500
commit3fa280d21878ae391674a21758199df3d2d8c3b5 (patch)
treef70c6ecafffeecc8e7a59dc9acef66c59a9ea54a /packages/desktop/scripts
parent1d58b5548287a3e32ffce3abdcf0f2db08fdb155 (diff)
downloadopencode-3fa280d21878ae391674a21758199df3d2d8c3b5.tar.gz
opencode-3fa280d21878ae391674a21758199df3d2d8c3b5.zip
chore: app -> desktop
Diffstat (limited to 'packages/desktop/scripts')
-rw-r--r--packages/desktop/scripts/vite-theme-plugin.ts163
1 files changed, 163 insertions, 0 deletions
diff --git a/packages/desktop/scripts/vite-theme-plugin.ts b/packages/desktop/scripts/vite-theme-plugin.ts
new file mode 100644
index 000000000..1241ffcd7
--- /dev/null
+++ b/packages/desktop/scripts/vite-theme-plugin.ts
@@ -0,0 +1,163 @@
+import type { Plugin } from "vite"
+import { readdir, readFile, writeFile } from "fs/promises"
+import { join, resolve } from "path"
+
+interface ThemeDefinition {
+ $schema?: string
+ defs?: Record<string, string>
+ theme: Record<string, any>
+}
+
+interface ResolvedThemeColor {
+ dark: string
+ light: string
+}
+
+class ColorResolver {
+ private colors: Map<string, any> = new Map()
+ private visited: Set<string> = new Set()
+
+ constructor(defs: Record<string, string> = {}, theme: Record<string, any> = {}) {
+ Object.entries(defs).forEach(([key, value]) => {
+ this.colors.set(key, value)
+ })
+ Object.entries(theme).forEach(([key, value]) => {
+ this.colors.set(key, value)
+ })
+ }
+
+ resolveColor(key: string, value: any): ResolvedThemeColor {
+ if (this.visited.has(key)) {
+ throw new Error(`Circular reference detected for color ${key}`)
+ }
+
+ this.visited.add(key)
+
+ try {
+ if (typeof value === "string") {
+ if (value === "none") return { dark: value, light: value }
+ if (value.startsWith("#")) {
+ return { dark: value.toLowerCase(), light: value.toLowerCase() }
+ }
+ const resolved = this.resolveReference(value)
+ return { dark: resolved, light: resolved }
+ }
+ if (typeof value === "object" && value !== null) {
+ const dark = this.resolveColorValue(value.dark || value.light || "#000000")
+ const light = this.resolveColorValue(value.light || value.dark || "#FFFFFF")
+ return { dark, light }
+ }
+ return { dark: "#000000", light: "#FFFFFF" }
+ } finally {
+ this.visited.delete(key)
+ }
+ }
+
+ private resolveColorValue(value: any): string {
+ if (typeof value === "string") {
+ if (value === "none") return value
+ if (value.startsWith("#")) {
+ return value.toLowerCase()
+ }
+ return this.resolveReference(value)
+ }
+ return value
+ }
+
+ private resolveReference(ref: string): string {
+ const colorValue = this.colors.get(ref)
+ if (colorValue === undefined) {
+ throw new Error(`Color reference '${ref}' not found`)
+ }
+ if (typeof colorValue === "string") {
+ if (colorValue === "none") return colorValue
+ if (colorValue.startsWith("#")) {
+ return colorValue.toLowerCase()
+ }
+ return this.resolveReference(colorValue)
+ }
+ return colorValue
+ }
+}
+
+function kebabCase(str: string): string {
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
+}
+
+function parseTheme(themeData: ThemeDefinition): Record<string, ResolvedThemeColor> {
+ const resolver = new ColorResolver(themeData.defs, themeData.theme)
+ const colors: Record<string, ResolvedThemeColor> = {}
+ Object.entries(themeData.theme).forEach(([key, value]) => {
+ colors[key] = resolver.resolveColor(key, value)
+ })
+ return colors
+}
+
+async function loadThemes(): Promise<Record<string, Record<string, ResolvedThemeColor>>> {
+ const themesDir = resolve(__dirname, "../../tui/internal/theme/themes")
+ const files = await readdir(themesDir)
+ const themes: Record<string, Record<string, ResolvedThemeColor>> = {}
+
+ for (const file of files) {
+ if (!file.endsWith(".json")) continue
+
+ const themeName = file.replace(".json", "")
+ const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8"))
+
+ themes[themeName] = parseTheme(themeData)
+ }
+
+ return themes
+}
+
+function generateCSS(themes: Record<string, Record<string, ResolvedThemeColor>>): string {
+ let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n`
+
+ const defaultTheme = themes["opencode"] || Object.values(themes)[0]
+ if (defaultTheme) {
+ Object.entries(defaultTheme).forEach(([key, color]) => {
+ const cssVar = `--theme-${kebabCase(key)}`
+ css += ` ${cssVar}: ${color.light};\n`
+ })
+ }
+ css += `}\n\n`
+
+ Object.entries(themes).forEach(([themeName, colors]) => {
+ css += `[data-theme="${themeName}"][data-dark="false"] {\n`
+ Object.entries(colors).forEach(([key, color]) => {
+ const cssVar = `--theme-${kebabCase(key)}`
+ css += ` ${cssVar}: ${color.light};\n`
+ })
+ css += `}\n\n`
+
+ css += `[data-theme="${themeName}"][data-dark="true"] {\n`
+ Object.entries(colors).forEach(([key, color]) => {
+ const cssVar = `--theme-${kebabCase(key)}`
+ css += ` ${cssVar}: ${color.dark};\n`
+ })
+ css += `}\n\n`
+ })
+
+ return css
+}
+
+export function generateThemeCSS(): Plugin {
+ return {
+ name: "generate-theme-css",
+ async buildStart() {
+ try {
+ console.log("Generating theme CSS...")
+ const themes = await loadThemes()
+ const css = generateCSS(themes)
+
+ const outputPath = resolve(__dirname, "../src/assets/theme.css")
+ await writeFile(outputPath, css)
+
+ console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`)
+ console.log(` Output: ${outputPath}`)
+ } catch (error) {
+ throw new Error(`Theme CSS generation failed: ${error}`)
+ }
+ },
+ }
+}