opencode_offline/packages/opencode/src/cli/cmd/tui/context/theme.tsx

673 lines
17 KiB
TypeScript
Raw Normal View History

2025-11-01 03:07:36 +08:00
import { SyntaxStyle, RGBA } from "@opentui/core"
2025-11-01 06:04:28 +08:00
import { createMemo, createSignal } from "solid-js"
2025-11-01 03:07:36 +08:00
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
import aura from "../../../../../../tui/internal/theme/themes/aura.json" with { type: "json" }
import ayu from "../../../../../../tui/internal/theme/themes/ayu.json" with { type: "json" }
import catppuccin from "../../../../../../tui/internal/theme/themes/catppuccin.json" with { type: "json" }
import cobalt2 from "../../../../../../tui/internal/theme/themes/cobalt2.json" with { type: "json" }
import dracula from "../../../../../../tui/internal/theme/themes/dracula.json" with { type: "json" }
import everforest from "../../../../../../tui/internal/theme/themes/everforest.json" with { type: "json" }
import github from "../../../../../../tui/internal/theme/themes/github.json" with { type: "json" }
import gruvbox from "../../../../../../tui/internal/theme/themes/gruvbox.json" with { type: "json" }
import kanagawa from "../../../../../../tui/internal/theme/themes/kanagawa.json" with { type: "json" }
import material from "../../../../../../tui/internal/theme/themes/material.json" with { type: "json" }
import matrix from "../../../../../../tui/internal/theme/themes/matrix.json" with { type: "json" }
import monokai from "../../../../../../tui/internal/theme/themes/monokai.json" with { type: "json" }
import nord from "../../../../../../tui/internal/theme/themes/nord.json" with { type: "json" }
import onedark from "../../../../../../tui/internal/theme/themes/one-dark.json" with { type: "json" }
import opencode from "../../../../../../tui/internal/theme/themes/opencode.json" with { type: "json" }
import palenight from "../../../../../../tui/internal/theme/themes/palenight.json" with { type: "json" }
import rosepine from "../../../../../../tui/internal/theme/themes/rosepine.json" with { type: "json" }
import solarized from "../../../../../../tui/internal/theme/themes/solarized.json" with { type: "json" }
import synthwave84 from "../../../../../../tui/internal/theme/themes/synthwave84.json" with { type: "json" }
import tokyonight from "../../../../../../tui/internal/theme/themes/tokyonight.json" with { type: "json" }
import vesper from "../../../../../../tui/internal/theme/themes/vesper.json" with { type: "json" }
import zenburn from "../../../../../../tui/internal/theme/themes/zenburn.json" with { type: "json" }
import { useKV } from "./kv"
2025-11-01 03:07:36 +08:00
type Theme = {
primary: RGBA
secondary: RGBA
accent: RGBA
error: RGBA
warning: RGBA
success: RGBA
info: RGBA
text: RGBA
textMuted: RGBA
background: RGBA
backgroundPanel: RGBA
backgroundElement: 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
2025-11-02 01:54:01 +08:00
syntaxComment: RGBA
syntaxKeyword: RGBA
syntaxFunction: RGBA
syntaxVariable: RGBA
syntaxString: RGBA
syntaxNumber: RGBA
syntaxType: RGBA
syntaxOperator: RGBA
syntaxPunctuation: RGBA
2025-11-01 03:07:36 +08:00
}
type HexColor = `#${string}`
type RefName = string
2025-11-02 00:14:10 +08:00
type Variant = {
2025-11-01 03:07:36 +08:00
dark: HexColor | RefName
light: HexColor | RefName
}
2025-11-02 00:14:10 +08:00
type ColorValue = HexColor | RefName | Variant
2025-11-01 03:07:36 +08:00
type ThemeJson = {
$schema?: string
defs?: Record<string, HexColor | RefName>
theme: Record<keyof Theme, ColorValue>
}
2025-11-02 01:54:01 +08:00
export const THEMES: Record<string, ThemeJson> = {
aura,
ayu,
catppuccin,
cobalt2,
dracula,
everforest,
github,
gruvbox,
kanagawa,
material,
matrix,
monokai,
nord,
["one-dark"]: onedark,
opencode,
palenight,
rosepine,
solarized,
synthwave84,
tokyonight,
vesper,
zenburn,
2025-11-01 03:07:36 +08:00
}
2025-11-02 01:54:01 +08:00
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
2025-11-01 03:07:36 +08:00
const defs = theme.defs ?? {}
function resolveColor(c: ColorValue): RGBA {
if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c])
2025-11-02 01:54:01 +08:00
return resolveColor(c[mode])
2025-11-01 03:07:36 +08:00
}
return Object.fromEntries(
Object.entries(theme.theme).map(([key, value]) => {
return [key, resolveColor(value)]
}),
) as Theme
}
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme",
2025-11-02 01:54:01 +08:00
init: (props: { mode: "dark" | "light" }) => {
2025-11-01 03:07:36 +08:00
const sync = useSync()
const kv = useKV()
2025-11-01 03:07:36 +08:00
2025-11-01 23:40:21 +08:00
const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.get("theme", "opencode"))
2025-11-02 01:54:01 +08:00
const [mode, setMode] = createSignal(props.mode)
2025-11-01 06:04:28 +08:00
const values = createMemo(() => {
2025-11-02 01:54:01 +08:00
return resolveTheme(THEMES[theme()] ?? THEMES.opencode, mode())
})
const syntax = createMemo(() => {
return SyntaxStyle.fromTheme([
{
scope: ["prompt"],
style: {
foreground: values().accent,
},
},
{
scope: ["extmark.file"],
style: {
foreground: values().warning,
bold: true,
},
},
{
scope: ["extmark.agent"],
style: {
foreground: values().secondary,
bold: true,
},
},
{
scope: ["extmark.paste"],
style: {
foreground: values().background,
background: values().warning,
bold: true,
},
},
{
scope: ["comment"],
style: {
foreground: values().syntaxComment,
italic: true,
},
},
{
scope: ["comment.documentation"],
style: {
foreground: values().syntaxComment,
italic: true,
},
},
{
scope: ["string", "symbol"],
style: {
foreground: values().syntaxString,
},
},
{
scope: ["number", "boolean"],
style: {
foreground: values().syntaxNumber,
},
},
{
scope: ["character.special"],
style: {
foreground: values().syntaxString,
},
},
{
scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
style: {
foreground: values().syntaxKeyword,
italic: true,
},
},
{
scope: ["keyword.type"],
style: {
foreground: values().syntaxType,
bold: true,
italic: true,
},
},
{
scope: ["keyword.function", "function.method"],
style: {
foreground: values().syntaxFunction,
},
},
{
scope: ["keyword"],
style: {
foreground: values().syntaxKeyword,
italic: true,
},
},
{
scope: ["keyword.import"],
style: {
foreground: values().syntaxKeyword,
},
},
{
scope: ["operator", "keyword.operator", "punctuation.delimiter"],
style: {
foreground: values().syntaxOperator,
},
},
{
scope: ["keyword.conditional.ternary"],
style: {
foreground: values().syntaxOperator,
},
},
{
scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
style: {
foreground: values().syntaxVariable,
},
},
{
scope: ["variable.member", "function", "constructor"],
style: {
foreground: values().syntaxFunction,
},
},
{
scope: ["type", "module"],
style: {
foreground: values().syntaxType,
},
},
{
scope: ["constant"],
style: {
foreground: values().syntaxNumber,
},
},
{
scope: ["property"],
style: {
foreground: values().syntaxVariable,
},
},
{
scope: ["class"],
style: {
foreground: values().syntaxType,
},
},
{
scope: ["parameter"],
style: {
foreground: values().syntaxVariable,
},
},
{
scope: ["punctuation", "punctuation.bracket"],
style: {
foreground: values().syntaxPunctuation,
},
},
{
scope: [
"variable.builtin",
"type.builtin",
"function.builtin",
"module.builtin",
"constant.builtin",
],
style: {
foreground: values().error,
},
},
{
scope: ["variable.super"],
style: {
foreground: values().error,
},
},
{
scope: ["string.escape", "string.regexp"],
style: {
foreground: values().syntaxKeyword,
},
},
{
scope: ["keyword.directive"],
style: {
foreground: values().syntaxKeyword,
italic: true,
},
},
{
scope: ["punctuation.special"],
style: {
foreground: values().syntaxOperator,
},
},
{
scope: ["keyword.modifier"],
style: {
foreground: values().syntaxKeyword,
italic: true,
},
},
{
scope: ["keyword.exception"],
style: {
foreground: values().syntaxKeyword,
italic: true,
},
},
// Markdown specific styles
{
scope: ["markup.heading"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.1"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.2"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.3"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.4"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.5"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.heading.6"],
style: {
foreground: values().markdownHeading,
bold: true,
},
},
{
scope: ["markup.bold", "markup.strong"],
style: {
foreground: values().markdownStrong,
bold: true,
},
},
{
scope: ["markup.italic"],
style: {
foreground: values().markdownEmph,
italic: true,
},
},
{
scope: ["markup.list"],
style: {
foreground: values().markdownListItem,
},
},
{
scope: ["markup.quote"],
style: {
foreground: values().markdownBlockQuote,
italic: true,
},
},
{
scope: ["markup.raw", "markup.raw.block"],
style: {
foreground: values().markdownCode,
},
},
{
scope: ["markup.raw.inline"],
style: {
foreground: values().markdownCode,
background: values().background,
},
},
{
scope: ["markup.link"],
style: {
foreground: values().markdownLink,
underline: true,
},
},
{
scope: ["markup.link.label"],
style: {
foreground: values().markdownLinkText,
underline: true,
},
},
{
scope: ["markup.link.url"],
style: {
foreground: values().markdownLink,
underline: true,
},
},
{
scope: ["label"],
style: {
foreground: values().markdownLinkText,
},
},
{
scope: ["spell", "nospell"],
style: {
foreground: values().text,
},
},
{
scope: ["conceal"],
style: {
foreground: values().textMuted,
},
},
// Additional common highlight groups
{
scope: ["string.special", "string.special.url"],
style: {
foreground: values().markdownLink,
underline: true,
},
},
{
scope: ["character"],
style: {
foreground: values().syntaxString,
},
},
{
scope: ["float"],
style: {
foreground: values().syntaxNumber,
},
},
{
scope: ["comment.error"],
style: {
foreground: values().error,
italic: true,
bold: true,
},
},
{
scope: ["comment.warning"],
style: {
foreground: values().warning,
italic: true,
bold: true,
},
},
{
scope: ["comment.todo", "comment.note"],
style: {
foreground: values().info,
italic: true,
bold: true,
},
},
{
scope: ["namespace"],
style: {
foreground: values().syntaxType,
},
},
{
scope: ["field"],
style: {
foreground: values().syntaxVariable,
},
},
{
scope: ["type.definition"],
style: {
foreground: values().syntaxType,
bold: true,
},
},
{
scope: ["keyword.export"],
style: {
foreground: values().syntaxKeyword,
},
},
{
scope: ["attribute", "annotation"],
style: {
foreground: values().warning,
},
},
{
scope: ["tag"],
style: {
foreground: values().error,
},
},
{
scope: ["tag.attribute"],
style: {
foreground: values().syntaxKeyword,
},
},
{
scope: ["tag.delimiter"],
style: {
foreground: values().syntaxOperator,
},
},
{
scope: ["markup.strikethrough"],
style: {
foreground: values().textMuted,
},
},
{
scope: ["markup.underline"],
style: {
foreground: values().text,
underline: true,
},
},
{
scope: ["markup.list.checked"],
style: {
foreground: values().success,
},
},
{
scope: ["markup.list.unchecked"],
style: {
foreground: values().textMuted,
},
},
{
scope: ["diff.plus"],
style: {
foreground: values().diffAdded,
},
},
{
scope: ["diff.minus"],
style: {
foreground: values().diffRemoved,
},
},
{
scope: ["diff.delta"],
style: {
foreground: values().diffContext,
},
},
{
scope: ["error"],
style: {
foreground: values().error,
bold: true,
},
},
{
scope: ["warning"],
style: {
foreground: values().warning,
bold: true,
},
},
{
scope: ["info"],
style: {
foreground: values().info,
},
},
{
scope: ["debug"],
style: {
foreground: values().textMuted,
},
},
])
2025-11-01 03:07:36 +08:00
})
return {
2025-11-01 06:04:28 +08:00
theme: new Proxy(values(), {
get(_target, prop) {
// @ts-expect-error
return values()[prop]
},
}),
get selected() {
2025-11-01 23:40:21 +08:00
return theme()
},
2025-11-02 01:54:01 +08:00
syntax,
mode,
setMode(mode: "dark" | "light") {
setMode(mode)
},
set(theme: string) {
if (!THEMES[theme]) return
2025-11-01 06:04:28 +08:00
setTheme(theme)
kv.set("theme", theme)
},
2025-11-01 03:07:36 +08:00
get ready() {
return sync.ready
},
}
},
})