| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873 |
- import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
- import { createMemo } from "solid-js"
- import { useSync } from "@tui/context/sync"
- import { createSimpleContext } from "./helper"
- import aura from "./theme/aura.json" with { type: "json" }
- import ayu from "./theme/ayu.json" with { type: "json" }
- import catppuccin from "./theme/catppuccin.json" with { type: "json" }
- import cobalt2 from "./theme/cobalt2.json" with { type: "json" }
- import dracula from "./theme/dracula.json" with { type: "json" }
- import everforest from "./theme/everforest.json" with { type: "json" }
- import github from "./theme/github.json" with { type: "json" }
- import gruvbox from "./theme/gruvbox.json" with { type: "json" }
- import kanagawa from "./theme/kanagawa.json" with { type: "json" }
- import material from "./theme/material.json" with { type: "json" }
- import matrix from "./theme/matrix.json" with { type: "json" }
- import monokai from "./theme/monokai.json" with { type: "json" }
- import nightowl from "./theme/nightowl.json" with { type: "json" }
- import nord from "./theme/nord.json" with { type: "json" }
- import onedark from "./theme/one-dark.json" with { type: "json" }
- import opencode from "./theme/opencode.json" with { type: "json" }
- import palenight from "./theme/palenight.json" with { type: "json" }
- import rosepine from "./theme/rosepine.json" with { type: "json" }
- import solarized from "./theme/solarized.json" with { type: "json" }
- import synthwave84 from "./theme/synthwave84.json" with { type: "json" }
- import tokyonight from "./theme/tokyonight.json" with { type: "json" }
- import vesper from "./theme/vesper.json" with { type: "json" }
- import zenburn from "./theme/zenburn.json" with { type: "json" }
- import { useKV } from "./kv"
- import { useRenderer } from "@opentui/solid"
- import { createStore } from "solid-js/store"
- 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
- syntaxComment: RGBA
- syntaxKeyword: RGBA
- syntaxFunction: RGBA
- syntaxVariable: RGBA
- syntaxString: RGBA
- syntaxNumber: RGBA
- syntaxType: RGBA
- syntaxOperator: RGBA
- syntaxPunctuation: RGBA
- }
- type HexColor = `#${string}`
- type RefName = string
- type Variant = {
- dark: HexColor | RefName
- light: HexColor | RefName
- }
- type ColorValue = HexColor | RefName | Variant | RGBA
- type ThemeJson = {
- $schema?: string
- defs?: Record<string, HexColor | RefName>
- theme: Record<keyof Theme, ColorValue>
- }
- export const DEFAULT_THEMES: Record<string, ThemeJson> = {
- aura,
- ayu,
- catppuccin,
- cobalt2,
- dracula,
- everforest,
- github,
- gruvbox,
- kanagawa,
- material,
- matrix,
- monokai,
- nightowl,
- nord,
- ["one-dark"]: onedark,
- opencode,
- palenight,
- rosepine,
- solarized,
- synthwave84,
- tokyonight,
- vesper,
- zenburn,
- }
- function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
- const defs = theme.defs ?? {}
- function resolveColor(c: ColorValue): RGBA {
- if (c instanceof RGBA) return c
- if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c])
- return resolveColor(c[mode])
- }
- 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",
- init: (props: { mode: "dark" | "light" }) => {
- const sync = useSync()
- const kv = useKV()
- const [store, setStore] = createStore({
- themes: DEFAULT_THEMES,
- mode: props.mode,
- active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
- })
- const renderer = useRenderer()
- renderer
- .getPalette({
- size: 16,
- })
- .then((colors) => {
- if (!colors.palette[0]) return
- setStore("themes", "system", generateSystem(colors, store.mode))
- })
- const values = createMemo(() => {
- return resolveTheme(store.themes[store.active] ?? store.themes.opencode, store.mode)
- })
- const syntax = createMemo(() => generateSyntax(values()))
- return {
- theme: new Proxy(values(), {
- get(_target, prop) {
- // @ts-expect-error
- return values()[prop]
- },
- }),
- get selected() {
- return store.active
- },
- all() {
- return store.themes
- },
- syntax,
- mode() {
- return store.mode
- },
- setMode(mode: "dark" | "light") {
- setStore("mode", mode)
- },
- set(theme: string) {
- setStore("active", theme)
- kv.set("theme", theme)
- },
- get ready() {
- return sync.ready
- },
- }
- },
- })
- function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
- const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
- const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
- const palette = colors.palette.map((x) => RGBA.fromHex(x!))
- const isDark = mode == "dark"
- // Generate gray scale based on terminal background
- const grays = generateGrayScale(bg, isDark)
- const textMuted = generateMutedTextColor(bg, isDark)
- // ANSI color references
- const ansiColors = {
- black: palette[0],
- red: palette[1],
- green: palette[2],
- yellow: palette[3],
- blue: palette[4],
- magenta: palette[5],
- cyan: palette[6],
- white: palette[7],
- }
- return {
- theme: {
- // Primary colors using ANSI
- primary: ansiColors.cyan,
- secondary: ansiColors.magenta,
- accent: ansiColors.cyan,
- // Status colors using ANSI
- error: ansiColors.red,
- warning: ansiColors.yellow,
- success: ansiColors.green,
- info: ansiColors.cyan,
- // Text colors
- text: fg,
- textMuted,
- // Background colors
- background: bg,
- backgroundPanel: grays[2],
- backgroundElement: grays[3],
- // Border colors
- borderSubtle: grays[6],
- border: grays[7],
- borderActive: grays[8],
- // Diff colors
- diffAdded: ansiColors.green,
- diffRemoved: ansiColors.red,
- diffContext: grays[7],
- diffHunkHeader: grays[7],
- diffHighlightAdded: ansiColors.green,
- diffHighlightRemoved: ansiColors.red,
- diffAddedBg: grays[2],
- diffRemovedBg: grays[2],
- diffContextBg: grays[1],
- diffLineNumber: grays[6],
- diffAddedLineNumberBg: grays[3],
- diffRemovedLineNumberBg: grays[3],
- // Markdown colors
- markdownText: fg,
- markdownHeading: fg,
- markdownLink: ansiColors.blue,
- markdownLinkText: ansiColors.cyan,
- markdownCode: ansiColors.green,
- markdownBlockQuote: ansiColors.yellow,
- markdownEmph: ansiColors.yellow,
- markdownStrong: fg,
- markdownHorizontalRule: grays[7],
- markdownListItem: ansiColors.blue,
- markdownListEnumeration: ansiColors.cyan,
- markdownImage: ansiColors.blue,
- markdownImageText: ansiColors.cyan,
- markdownCodeBlock: fg,
- // Syntax colors
- syntaxComment: textMuted,
- syntaxKeyword: ansiColors.magenta,
- syntaxFunction: ansiColors.blue,
- syntaxVariable: fg,
- syntaxString: ansiColors.green,
- syntaxNumber: ansiColors.yellow,
- syntaxType: ansiColors.cyan,
- syntaxOperator: ansiColors.cyan,
- syntaxPunctuation: fg,
- },
- }
- }
- function generateGrayScale(bg: RGBA, isDark: boolean): Record<number, RGBA> {
- const grays: Record<number, RGBA> = {}
- // RGBA stores floats in range 0-1, convert to 0-255
- const bgR = bg.r * 255
- const bgG = bg.g * 255
- const bgB = bg.b * 255
- const luminance = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
- for (let i = 1; i <= 12; i++) {
- const factor = i / 12.0
- let grayValue: number
- let newR: number
- let newG: number
- let newB: number
- if (isDark) {
- if (luminance < 10) {
- grayValue = Math.floor(factor * 0.4 * 255)
- newR = grayValue
- newG = grayValue
- newB = grayValue
- } else {
- const newLum = luminance + (255 - luminance) * factor * 0.4
- const ratio = newLum / luminance
- newR = Math.min(bgR * ratio, 255)
- newG = Math.min(bgG * ratio, 255)
- newB = Math.min(bgB * ratio, 255)
- }
- } else {
- if (luminance > 245) {
- grayValue = Math.floor(255 - factor * 0.4 * 255)
- newR = grayValue
- newG = grayValue
- newB = grayValue
- } else {
- const newLum = luminance * (1 - factor * 0.4)
- const ratio = newLum / luminance
- newR = Math.max(bgR * ratio, 0)
- newG = Math.max(bgG * ratio, 0)
- newB = Math.max(bgB * ratio, 0)
- }
- }
- grays[i] = RGBA.fromInts(Math.floor(newR), Math.floor(newG), Math.floor(newB))
- }
- return grays
- }
- function generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA {
- // RGBA stores floats in range 0-1, convert to 0-255
- const bgR = bg.r * 255
- const bgG = bg.g * 255
- const bgB = bg.b * 255
- const bgLum = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
- let grayValue: number
- if (isDark) {
- if (bgLum < 10) {
- // Very dark/black background
- grayValue = 180 // #b4b4b4
- } else {
- // Scale up for lighter dark backgrounds
- grayValue = Math.min(Math.floor(160 + bgLum * 0.3), 200)
- }
- } else {
- if (bgLum > 245) {
- // Very light/white background
- grayValue = 75 // #4b4b4b
- } else {
- // Scale down for darker light backgrounds
- grayValue = Math.max(Math.floor(100 - (255 - bgLum) * 0.2), 60)
- }
- }
- return RGBA.fromInts(grayValue, grayValue, grayValue)
- }
- function generateSyntax(theme: Theme) {
- return SyntaxStyle.fromTheme([
- {
- scope: ["prompt"],
- style: {
- foreground: theme.accent,
- },
- },
- {
- scope: ["extmark.file"],
- style: {
- foreground: theme.warning,
- bold: true,
- },
- },
- {
- scope: ["extmark.agent"],
- style: {
- foreground: theme.secondary,
- bold: true,
- },
- },
- {
- scope: ["extmark.paste"],
- style: {
- foreground: theme.background,
- background: theme.warning,
- bold: true,
- },
- },
- {
- scope: ["comment"],
- style: {
- foreground: theme.syntaxComment,
- italic: true,
- },
- },
- {
- scope: ["comment.documentation"],
- style: {
- foreground: theme.syntaxComment,
- italic: true,
- },
- },
- {
- scope: ["string", "symbol"],
- style: {
- foreground: theme.syntaxString,
- },
- },
- {
- scope: ["number", "boolean"],
- style: {
- foreground: theme.syntaxNumber,
- },
- },
- {
- scope: ["character.special"],
- style: {
- foreground: theme.syntaxString,
- },
- },
- {
- scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
- style: {
- foreground: theme.syntaxKeyword,
- italic: true,
- },
- },
- {
- scope: ["keyword.type"],
- style: {
- foreground: theme.syntaxType,
- bold: true,
- italic: true,
- },
- },
- {
- scope: ["keyword.function", "function.method"],
- style: {
- foreground: theme.syntaxFunction,
- },
- },
- {
- scope: ["keyword"],
- style: {
- foreground: theme.syntaxKeyword,
- italic: true,
- },
- },
- {
- scope: ["keyword.import"],
- style: {
- foreground: theme.syntaxKeyword,
- },
- },
- {
- scope: ["operator", "keyword.operator", "punctuation.delimiter"],
- style: {
- foreground: theme.syntaxOperator,
- },
- },
- {
- scope: ["keyword.conditional.ternary"],
- style: {
- foreground: theme.syntaxOperator,
- },
- },
- {
- scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
- style: {
- foreground: theme.syntaxVariable,
- },
- },
- {
- scope: ["variable.member", "function", "constructor"],
- style: {
- foreground: theme.syntaxFunction,
- },
- },
- {
- scope: ["type", "module"],
- style: {
- foreground: theme.syntaxType,
- },
- },
- {
- scope: ["constant"],
- style: {
- foreground: theme.syntaxNumber,
- },
- },
- {
- scope: ["property"],
- style: {
- foreground: theme.syntaxVariable,
- },
- },
- {
- scope: ["class"],
- style: {
- foreground: theme.syntaxType,
- },
- },
- {
- scope: ["parameter"],
- style: {
- foreground: theme.syntaxVariable,
- },
- },
- {
- scope: ["punctuation", "punctuation.bracket"],
- style: {
- foreground: theme.syntaxPunctuation,
- },
- },
- {
- scope: [
- "variable.builtin",
- "type.builtin",
- "function.builtin",
- "module.builtin",
- "constant.builtin",
- ],
- style: {
- foreground: theme.error,
- },
- },
- {
- scope: ["variable.super"],
- style: {
- foreground: theme.error,
- },
- },
- {
- scope: ["string.escape", "string.regexp"],
- style: {
- foreground: theme.syntaxKeyword,
- },
- },
- {
- scope: ["keyword.directive"],
- style: {
- foreground: theme.syntaxKeyword,
- italic: true,
- },
- },
- {
- scope: ["punctuation.special"],
- style: {
- foreground: theme.syntaxOperator,
- },
- },
- {
- scope: ["keyword.modifier"],
- style: {
- foreground: theme.syntaxKeyword,
- italic: true,
- },
- },
- {
- scope: ["keyword.exception"],
- style: {
- foreground: theme.syntaxKeyword,
- italic: true,
- },
- },
- // Markdown specific styles
- {
- scope: ["markup.heading"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.1"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.2"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.3"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.4"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.5"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.heading.6"],
- style: {
- foreground: theme.markdownHeading,
- bold: true,
- },
- },
- {
- scope: ["markup.bold", "markup.strong"],
- style: {
- foreground: theme.markdownStrong,
- bold: true,
- },
- },
- {
- scope: ["markup.italic"],
- style: {
- foreground: theme.markdownEmph,
- italic: true,
- },
- },
- {
- scope: ["markup.list"],
- style: {
- foreground: theme.markdownListItem,
- },
- },
- {
- scope: ["markup.quote"],
- style: {
- foreground: theme.markdownBlockQuote,
- italic: true,
- },
- },
- {
- scope: ["markup.raw", "markup.raw.block"],
- style: {
- foreground: theme.markdownCode,
- },
- },
- {
- scope: ["markup.raw.inline"],
- style: {
- foreground: theme.markdownCode,
- background: theme.background,
- },
- },
- {
- scope: ["markup.link"],
- style: {
- foreground: theme.markdownLink,
- underline: true,
- },
- },
- {
- scope: ["markup.link.label"],
- style: {
- foreground: theme.markdownLinkText,
- underline: true,
- },
- },
- {
- scope: ["markup.link.url"],
- style: {
- foreground: theme.markdownLink,
- underline: true,
- },
- },
- {
- scope: ["label"],
- style: {
- foreground: theme.markdownLinkText,
- },
- },
- {
- scope: ["spell", "nospell"],
- style: {
- foreground: theme.text,
- },
- },
- {
- scope: ["conceal"],
- style: {
- foreground: theme.textMuted,
- },
- },
- // Additional common highlight groups
- {
- scope: ["string.special", "string.special.url"],
- style: {
- foreground: theme.markdownLink,
- underline: true,
- },
- },
- {
- scope: ["character"],
- style: {
- foreground: theme.syntaxString,
- },
- },
- {
- scope: ["float"],
- style: {
- foreground: theme.syntaxNumber,
- },
- },
- {
- scope: ["comment.error"],
- style: {
- foreground: theme.error,
- italic: true,
- bold: true,
- },
- },
- {
- scope: ["comment.warning"],
- style: {
- foreground: theme.warning,
- italic: true,
- bold: true,
- },
- },
- {
- scope: ["comment.todo", "comment.note"],
- style: {
- foreground: theme.info,
- italic: true,
- bold: true,
- },
- },
- {
- scope: ["namespace"],
- style: {
- foreground: theme.syntaxType,
- },
- },
- {
- scope: ["field"],
- style: {
- foreground: theme.syntaxVariable,
- },
- },
- {
- scope: ["type.definition"],
- style: {
- foreground: theme.syntaxType,
- bold: true,
- },
- },
- {
- scope: ["keyword.export"],
- style: {
- foreground: theme.syntaxKeyword,
- },
- },
- {
- scope: ["attribute", "annotation"],
- style: {
- foreground: theme.warning,
- },
- },
- {
- scope: ["tag"],
- style: {
- foreground: theme.error,
- },
- },
- {
- scope: ["tag.attribute"],
- style: {
- foreground: theme.syntaxKeyword,
- },
- },
- {
- scope: ["tag.delimiter"],
- style: {
- foreground: theme.syntaxOperator,
- },
- },
- {
- scope: ["markup.strikethrough"],
- style: {
- foreground: theme.textMuted,
- },
- },
- {
- scope: ["markup.underline"],
- style: {
- foreground: theme.text,
- underline: true,
- },
- },
- {
- scope: ["markup.list.checked"],
- style: {
- foreground: theme.success,
- },
- },
- {
- scope: ["markup.list.unchecked"],
- style: {
- foreground: theme.textMuted,
- },
- },
- {
- scope: ["diff.plus"],
- style: {
- foreground: theme.diffAdded,
- },
- },
- {
- scope: ["diff.minus"],
- style: {
- foreground: theme.diffRemoved,
- },
- },
- {
- scope: ["diff.delta"],
- style: {
- foreground: theme.diffContext,
- },
- },
- {
- scope: ["error"],
- style: {
- foreground: theme.error,
- bold: true,
- },
- },
- {
- scope: ["warning"],
- style: {
- foreground: theme.warning,
- bold: true,
- },
- },
- {
- scope: ["info"],
- style: {
- foreground: theme.info,
- },
- },
- {
- scope: ["debug"],
- style: {
- foreground: theme.textMuted,
- },
- },
- ])
- }
|