theme store
parent
f98ad6e078
commit
3f9603e7a6
|
|
@ -1,6 +1,6 @@
|
|||
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
||||
import path from "path"
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, onMount } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { Glob } from "../../../../util/glob"
|
||||
import aura from "./theme/aura.json" with { type: "json" }
|
||||
|
|
@ -138,44 +138,6 @@ type ThemeJson = {
|
|||
}
|
||||
}
|
||||
|
||||
type ThemeRegistry = {
|
||||
themes: Record<string, ThemeJson>
|
||||
listeners: Set<(themes: Record<string, ThemeJson>) => void>
|
||||
}
|
||||
|
||||
const registry: ThemeRegistry = {
|
||||
themes: {},
|
||||
listeners: new Set(),
|
||||
}
|
||||
|
||||
export function registerThemes(themes: Record<string, unknown>) {
|
||||
const entries = 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
|
||||
})
|
||||
if (entries.length === 0) return
|
||||
|
||||
for (const [name, theme] of entries) {
|
||||
registry.themes[name] = theme
|
||||
}
|
||||
|
||||
const payload = Object.fromEntries(entries)
|
||||
for (const handler of registry.listeners) {
|
||||
handler(payload)
|
||||
}
|
||||
}
|
||||
|
||||
function registeredThemes() {
|
||||
return registry.themes
|
||||
}
|
||||
|
||||
function onThemes(handler: (themes: Record<string, ThemeJson>) => void) {
|
||||
registry.listeners.add(handler)
|
||||
return () => registry.listeners.delete(handler)
|
||||
}
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
||||
aura,
|
||||
ayu,
|
||||
|
|
@ -212,6 +174,46 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
|||
carbonfox,
|
||||
}
|
||||
|
||||
type State = {
|
||||
themes: Record<string, ThemeJson>
|
||||
mode: "dark" | "light"
|
||||
active: string
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore<State>({
|
||||
themes: DEFAULT_THEMES,
|
||||
mode: "dark",
|
||||
active: "opencode",
|
||||
ready: false,
|
||||
})
|
||||
|
||||
function mergeThemes(themes: Record<string, ThemeJson>) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
for (const [name, theme] of Object.entries(themes)) {
|
||||
if (draft.themes[name]) continue
|
||||
draft.themes[name] = theme
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
if (!list.length) return
|
||||
mergeThemes(Object.fromEntries(list))
|
||||
}
|
||||
|
||||
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
const defs = theme.defs ?? {}
|
||||
function resolveColor(c: ColorValue): RGBA {
|
||||
|
|
@ -320,12 +322,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
init: (props: { mode: "dark" | "light" }) => {
|
||||
const config = useTuiConfig()
|
||||
const kv = useKV()
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES,
|
||||
mode: kv.get("theme_mode", props.mode),
|
||||
active: (config.theme ?? kv.get("theme", "opencode")) as string,
|
||||
ready: false,
|
||||
})
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.mode = kv.get("theme_mode", props.mode)
|
||||
draft.active = (config.theme ?? kv.get("theme", "opencode")) as string
|
||||
draft.ready = false
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const theme = config.theme
|
||||
|
|
@ -334,7 +338,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
|
||||
function init() {
|
||||
resolveSystemTheme()
|
||||
mergeThemes(registeredThemes())
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
setStore(
|
||||
|
|
@ -354,22 +357,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
}
|
||||
|
||||
onMount(init)
|
||||
onCleanup(
|
||||
onThemes((themes) => {
|
||||
mergeThemes(themes)
|
||||
}),
|
||||
)
|
||||
|
||||
function mergeThemes(themes: Record<string, ThemeJson>) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
for (const [name, theme] of Object.entries(themes)) {
|
||||
if (draft.themes[name]) continue
|
||||
draft.themes[name] = theme
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveSystemTheme() {
|
||||
console.log("resolveSystemTheme")
|
||||
|
|
@ -425,7 +412,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
return store.active
|
||||
},
|
||||
all() {
|
||||
return store.themes
|
||||
return allThemes()
|
||||
},
|
||||
syntax,
|
||||
subtleSyntax,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { expect, mock, test } from "bun:test"
|
||||
|
||||
mock.module("@opentui/solid/jsx-runtime", () => ({
|
||||
Fragment: Symbol.for("Fragment"),
|
||||
jsx: () => null,
|
||||
jsxs: () => null,
|
||||
jsxDEV: () => null,
|
||||
}))
|
||||
|
||||
const { DEFAULT_THEMES, allThemes, registerThemes } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
|
||||
test("registerThemes writes into module theme store", () => {
|
||||
const name = `plugin-theme-${Date.now()}`
|
||||
registerThemes({
|
||||
[name]: DEFAULT_THEMES.opencode,
|
||||
})
|
||||
|
||||
expect(allThemes()[name]).toBeDefined()
|
||||
})
|
||||
|
||||
test("registerThemes 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(allThemes()[name]).toBeDefined()
|
||||
expect(allThemes()[name]!.theme.primary).toBe("#101010")
|
||||
})
|
||||
|
||||
test("registerThemes ignores entries without a theme object", () => {
|
||||
const name = `plugin-theme-invalid-${Date.now()}`
|
||||
registerThemes({ [name]: { defs: { a: "#ffffff" } } })
|
||||
expect(allThemes()[name]).toBeUndefined()
|
||||
})
|
||||
Loading…
Reference in New Issue