mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 03:04:21 +01:00
system theme (#4010)
This commit is contained in:
20
bun.lock
20
bun.lock
@@ -185,8 +185,8 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.1.36",
|
||||
"@opentui/solid": "0.1.36",
|
||||
"@opentui/core": "0.0.0-20251106-788e97e4",
|
||||
"@opentui/solid": "0.0.0-20251106-788e97e4",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -962,21 +962,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.36", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.36", "@opentui/core-darwin-x64": "0.1.36", "@opentui/core-linux-arm64": "0.1.36", "@opentui/core-linux-x64": "0.1.36", "@opentui/core-win32-arm64": "0.1.36", "@opentui/core-win32-x64": "0.1.36", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-urDrj33udJ0dJGkZv+T5U0mCFBOOvUt9Tvqkrj8aRvi6kN0Bc5d2COuWcpAKo0TO9/PvjSwHC+CMnw2Sr46/ug=="],
|
||||
"@opentui/core": ["@opentui/core@0.0.0-20251106-788e97e4", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-darwin-x64": "0.0.0-20251106-788e97e4", "@opentui/core-linux-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-linux-x64": "0.0.0-20251106-788e97e4", "@opentui/core-win32-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-win32-x64": "0.0.0-20251106-788e97e4", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Es2Oe7/J/yb58e0jjq/04pV9Mekx6hM4go4C5uTiZksX3asfIGWk553cuf5WlWj0PDlVnC+s7Nnayi/NbLJ5jQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.36", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/fb0k1H0CeTroVt2UoEAcVrEx1cIYy4B2zfX0MrwUkIfXi36aoIBnisBeYvyCpsQfxFAkyLYCCA3NzaYEyC5hg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251106-788e97e4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EOO8SSIYJBIh+Sd9bgVTiQmt+TEJmfg65/oym54J4zfDtCYlAqSaLcRnDe4TzB+4hejV9of8etrG3ZZACBJT+A=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.36", "", { "os": "darwin", "cpu": "x64" }, "sha512-PZMydJbSDUoEWqZsyEV8+FSwMT+r7mWFL0ABgdALI3AOrSr7Z8dMcRnFWl8LhriuHS589THvETJEN28L4q/E2Q=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251106-788e97e4", "", { "os": "darwin", "cpu": "x64" }, "sha512-MUTt7CDbzL2afNGK8gJ4jUZd+AHiOUJEO0eJGDSfWU8DUs0zv8XoLZfaI5PPbkUPEL/7CEBMARAAiwfRtoG/4A=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.36", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATR+vdtraZEC/gHR1mQa/NYPlqFNBpsnnJAGepQmcxm85VceLYM701QaaIgNAwyYXiP6RQN1ZCv06MD1Ph1m4w=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251106-788e97e4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zi1EzLCzooRfYoQnN/Dz8OxzrpRXByny8SJqhdO9ZP2mYX72yJ3AhUUW1Sl6YSzVi0H+QIKj7g+RX2KfsXIGFg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.36", "", { "os": "linux", "cpu": "x64" }, "sha512-INsnPtcZVx68C+0Vd0L9+akDwNbWblUDqLmY9CftfmeLFubzvJXNRYTBvr7lX68fcst6Ho+0beUxyUoClKc0rg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251106-788e97e4", "", { "os": "linux", "cpu": "x64" }, "sha512-/E0XEBVzO4JEEhJGzfURF2tPxDE2oTODxlgNYYB1QbAuOsLcV69uSrwAjo1TxuIn4P78tBR+ZOlmONjroPqfbQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.36", "", { "os": "win32", "cpu": "arm64" }, "sha512-x9lDZTL+xE8jsG1hP4pdsqCsZBu77JNR/ze5F7ZQkYQEC6Zl/XJtL1YT08nUlWOu4NMSws2xXV0lS/sJkbEgPA=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251106-788e97e4", "", { "os": "win32", "cpu": "arm64" }, "sha512-En/29cgpYVvzlrQ7fAoP+EUdzmczgMzBIGGM0RuLi2hmCmCqyMtOJ0EJUh9UXa5jYIXNGOP49sIP6bUBbvXt7g=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.36", "", { "os": "win32", "cpu": "x64" }, "sha512-WVU+qtAfJe8ikPWbw8Hfli15GuQTMKiceTkF5lql5AQYy7PKYtGTzWszxOZKeUU1/eEd2X4REi8Bn0TprEMxYw=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251106-788e97e4", "", { "os": "win32", "cpu": "x64" }, "sha512-2lu0bgEi+k/1c9VHQFg3wjVxMgQnuZhs/6sDDpxk9eNS3fuHEJfZi0PFJQk2J4IFQL61nzukOvJKgYDWQvKB1g=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.36", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.36", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-oHI01kZgyNecvXRFyQKJEDC5TCcsvfTPxHCa/XjbcZzH2qE2rfYMUF0mpwlLqoY9b3pm3w7Tpa8upzi1euBGJg=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.0.0-20251106-788e97e4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251106-788e97e4", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-82rFS6BB60rJZU5Ad8Wf58V6HaMSkpnjciizkv3vsjJc9hvIAwLRNYqPypQB+etypuELhYMzzaVqt+wUsPHSqQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.1.36",
|
||||
"@opentui/solid": "0.1.36",
|
||||
"@opentui/core": "0.0.0-20251106-788e97e4",
|
||||
"@opentui/solid": "0.0.0-20251106-788e97e4",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
|
||||
import { THEMES, useTheme } from "../context/theme"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
|
||||
export function DialogThemeList() {
|
||||
const theme = useTheme()
|
||||
const options = Object.keys(THEMES).map((value) => ({
|
||||
const options = Object.keys(theme.all()).map((value) => ({
|
||||
title: value,
|
||||
value: value as keyof typeof THEMES,
|
||||
value: value,
|
||||
}))
|
||||
const dialog = useDialog()
|
||||
let confirmed = false
|
||||
let ref: DialogSelectRef<keyof typeof THEMES>
|
||||
let ref: DialogSelectRef<string>
|
||||
const initial = theme.selected
|
||||
|
||||
onMount(() => {
|
||||
// highlight the first theme in the list when we open it for UX
|
||||
theme.set(Object.keys(THEMES)[0] as keyof typeof THEMES)
|
||||
theme.set(Object.keys(theme.all())[0])
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
// if we close the dialog without confirming, reset back to the initial theme
|
||||
if (!confirmed) theme.set(initial)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SyntaxStyle, RGBA } from "@opentui/core"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
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" }
|
||||
@@ -26,6 +26,8 @@ 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
|
||||
@@ -86,14 +88,14 @@ type Variant = {
|
||||
dark: HexColor | RefName
|
||||
light: HexColor | RefName
|
||||
}
|
||||
type ColorValue = HexColor | RefName | Variant
|
||||
type ColorValue = HexColor | RefName | Variant | RGBA
|
||||
type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Record<keyof Theme, ColorValue>
|
||||
}
|
||||
|
||||
export const THEMES: Record<string, ThemeJson> = {
|
||||
export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
||||
aura,
|
||||
ayu,
|
||||
catppuccin,
|
||||
@@ -122,6 +124,7 @@ export const THEMES: Record<string, ThemeJson> = {
|
||||
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])
|
||||
}
|
||||
@@ -137,87 +140,310 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
init: (props: { mode: "dark" | "light" }) => {
|
||||
const sync = useSync()
|
||||
const kv = useKV()
|
||||
|
||||
const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.get("theme", "opencode"))
|
||||
const [mode, setMode] = createSignal(props.mode)
|
||||
|
||||
const values = createMemo(() => {
|
||||
return resolveTheme(THEMES[theme()] ?? THEMES.opencode, mode())
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES,
|
||||
mode: props.mode,
|
||||
active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
|
||||
})
|
||||
|
||||
const syntax = createMemo(() => {
|
||||
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: values().accent,
|
||||
foreground: theme.accent,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["extmark.file"],
|
||||
style: {
|
||||
foreground: values().warning,
|
||||
foreground: theme.warning,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["extmark.agent"],
|
||||
style: {
|
||||
foreground: values().secondary,
|
||||
foreground: theme.secondary,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["extmark.paste"],
|
||||
style: {
|
||||
foreground: values().background,
|
||||
background: values().warning,
|
||||
foreground: theme.background,
|
||||
background: theme.warning,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["comment"],
|
||||
style: {
|
||||
foreground: values().syntaxComment,
|
||||
foreground: theme.syntaxComment,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["comment.documentation"],
|
||||
style: {
|
||||
foreground: values().syntaxComment,
|
||||
foreground: theme.syntaxComment,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["string", "symbol"],
|
||||
style: {
|
||||
foreground: values().syntaxString,
|
||||
foreground: theme.syntaxString,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["number", "boolean"],
|
||||
style: {
|
||||
foreground: values().syntaxNumber,
|
||||
foreground: theme.syntaxNumber,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["character.special"],
|
||||
style: {
|
||||
foreground: values().syntaxString,
|
||||
foreground: theme.syntaxString,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.type"],
|
||||
style: {
|
||||
foreground: values().syntaxType,
|
||||
foreground: theme.syntaxType,
|
||||
bold: true,
|
||||
italic: true,
|
||||
},
|
||||
@@ -225,80 +451,80 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
{
|
||||
scope: ["keyword.function", "function.method"],
|
||||
style: {
|
||||
foreground: values().syntaxFunction,
|
||||
foreground: theme.syntaxFunction,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.import"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["operator", "keyword.operator", "punctuation.delimiter"],
|
||||
style: {
|
||||
foreground: values().syntaxOperator,
|
||||
foreground: theme.syntaxOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.conditional.ternary"],
|
||||
style: {
|
||||
foreground: values().syntaxOperator,
|
||||
foreground: theme.syntaxOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
|
||||
style: {
|
||||
foreground: values().syntaxVariable,
|
||||
foreground: theme.syntaxVariable,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["variable.member", "function", "constructor"],
|
||||
style: {
|
||||
foreground: values().syntaxFunction,
|
||||
foreground: theme.syntaxFunction,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["type", "module"],
|
||||
style: {
|
||||
foreground: values().syntaxType,
|
||||
foreground: theme.syntaxType,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["constant"],
|
||||
style: {
|
||||
foreground: values().syntaxNumber,
|
||||
foreground: theme.syntaxNumber,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["property"],
|
||||
style: {
|
||||
foreground: values().syntaxVariable,
|
||||
foreground: theme.syntaxVariable,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["class"],
|
||||
style: {
|
||||
foreground: values().syntaxType,
|
||||
foreground: theme.syntaxType,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["parameter"],
|
||||
style: {
|
||||
foreground: values().syntaxVariable,
|
||||
foreground: theme.syntaxVariable,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "punctuation.bracket"],
|
||||
style: {
|
||||
foreground: values().syntaxPunctuation,
|
||||
foreground: theme.syntaxPunctuation,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,45 +536,45 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
"constant.builtin",
|
||||
],
|
||||
style: {
|
||||
foreground: values().error,
|
||||
foreground: theme.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["variable.super"],
|
||||
style: {
|
||||
foreground: values().error,
|
||||
foreground: theme.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["string.escape", "string.regexp"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.directive"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["punctuation.special"],
|
||||
style: {
|
||||
foreground: values().syntaxOperator,
|
||||
foreground: theme.syntaxOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.modifier"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.exception"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
@@ -356,155 +582,155 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
{
|
||||
scope: ["markup.heading"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.1"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.2"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.3"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.4"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.5"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading.6"],
|
||||
style: {
|
||||
foreground: values().markdownHeading,
|
||||
foreground: theme.markdownHeading,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.bold", "markup.strong"],
|
||||
style: {
|
||||
foreground: values().markdownStrong,
|
||||
foreground: theme.markdownStrong,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.italic"],
|
||||
style: {
|
||||
foreground: values().markdownEmph,
|
||||
foreground: theme.markdownEmph,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.list"],
|
||||
style: {
|
||||
foreground: values().markdownListItem,
|
||||
foreground: theme.markdownListItem,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.quote"],
|
||||
style: {
|
||||
foreground: values().markdownBlockQuote,
|
||||
foreground: theme.markdownBlockQuote,
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.raw", "markup.raw.block"],
|
||||
style: {
|
||||
foreground: values().markdownCode,
|
||||
foreground: theme.markdownCode,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.raw.inline"],
|
||||
style: {
|
||||
foreground: values().markdownCode,
|
||||
background: values().background,
|
||||
foreground: theme.markdownCode,
|
||||
background: theme.background,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.link"],
|
||||
style: {
|
||||
foreground: values().markdownLink,
|
||||
foreground: theme.markdownLink,
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.link.label"],
|
||||
style: {
|
||||
foreground: values().markdownLinkText,
|
||||
foreground: theme.markdownLinkText,
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.link.url"],
|
||||
style: {
|
||||
foreground: values().markdownLink,
|
||||
foreground: theme.markdownLink,
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["label"],
|
||||
style: {
|
||||
foreground: values().markdownLinkText,
|
||||
foreground: theme.markdownLinkText,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["spell", "nospell"],
|
||||
style: {
|
||||
foreground: values().text,
|
||||
foreground: theme.text,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["conceal"],
|
||||
style: {
|
||||
foreground: values().textMuted,
|
||||
foreground: theme.textMuted,
|
||||
},
|
||||
},
|
||||
// Additional common highlight groups
|
||||
{
|
||||
scope: ["string.special", "string.special.url"],
|
||||
style: {
|
||||
foreground: values().markdownLink,
|
||||
foreground: theme.markdownLink,
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["character"],
|
||||
style: {
|
||||
foreground: values().syntaxString,
|
||||
foreground: theme.syntaxString,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["float"],
|
||||
style: {
|
||||
foreground: values().syntaxNumber,
|
||||
foreground: theme.syntaxNumber,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["comment.error"],
|
||||
style: {
|
||||
foreground: values().error,
|
||||
foreground: theme.error,
|
||||
italic: true,
|
||||
bold: true,
|
||||
},
|
||||
@@ -512,7 +738,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
{
|
||||
scope: ["comment.warning"],
|
||||
style: {
|
||||
foreground: values().warning,
|
||||
foreground: theme.warning,
|
||||
italic: true,
|
||||
bold: true,
|
||||
},
|
||||
@@ -520,7 +746,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
{
|
||||
scope: ["comment.todo", "comment.note"],
|
||||
style: {
|
||||
foreground: values().info,
|
||||
foreground: theme.info,
|
||||
italic: true,
|
||||
bold: true,
|
||||
},
|
||||
@@ -528,147 +754,120 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
{
|
||||
scope: ["namespace"],
|
||||
style: {
|
||||
foreground: values().syntaxType,
|
||||
foreground: theme.syntaxType,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["field"],
|
||||
style: {
|
||||
foreground: values().syntaxVariable,
|
||||
foreground: theme.syntaxVariable,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["type.definition"],
|
||||
style: {
|
||||
foreground: values().syntaxType,
|
||||
foreground: theme.syntaxType,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["keyword.export"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["attribute", "annotation"],
|
||||
style: {
|
||||
foreground: values().warning,
|
||||
foreground: theme.warning,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["tag"],
|
||||
style: {
|
||||
foreground: values().error,
|
||||
foreground: theme.error,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["tag.attribute"],
|
||||
style: {
|
||||
foreground: values().syntaxKeyword,
|
||||
foreground: theme.syntaxKeyword,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["tag.delimiter"],
|
||||
style: {
|
||||
foreground: values().syntaxOperator,
|
||||
foreground: theme.syntaxOperator,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.strikethrough"],
|
||||
style: {
|
||||
foreground: values().textMuted,
|
||||
foreground: theme.textMuted,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.underline"],
|
||||
style: {
|
||||
foreground: values().text,
|
||||
foreground: theme.text,
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.list.checked"],
|
||||
style: {
|
||||
foreground: values().success,
|
||||
foreground: theme.success,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["markup.list.unchecked"],
|
||||
style: {
|
||||
foreground: values().textMuted,
|
||||
foreground: theme.textMuted,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["diff.plus"],
|
||||
style: {
|
||||
foreground: values().diffAdded,
|
||||
foreground: theme.diffAdded,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["diff.minus"],
|
||||
style: {
|
||||
foreground: values().diffRemoved,
|
||||
foreground: theme.diffRemoved,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["diff.delta"],
|
||||
style: {
|
||||
foreground: values().diffContext,
|
||||
foreground: theme.diffContext,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["error"],
|
||||
style: {
|
||||
foreground: values().error,
|
||||
foreground: theme.error,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["warning"],
|
||||
style: {
|
||||
foreground: values().warning,
|
||||
foreground: theme.warning,
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["info"],
|
||||
style: {
|
||||
foreground: values().info,
|
||||
foreground: theme.info,
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: ["debug"],
|
||||
style: {
|
||||
foreground: values().textMuted,
|
||||
foreground: theme.textMuted,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
return {
|
||||
theme: new Proxy(values(), {
|
||||
get(_target, prop) {
|
||||
// @ts-expect-error
|
||||
return values()[prop]
|
||||
},
|
||||
}),
|
||||
get selected() {
|
||||
return theme()
|
||||
},
|
||||
syntax,
|
||||
mode,
|
||||
setMode(mode: "dark" | "light") {
|
||||
setMode(mode)
|
||||
},
|
||||
set(theme: string) {
|
||||
if (!THEMES[theme]) return
|
||||
setTheme(theme)
|
||||
kv.set("theme", theme)
|
||||
},
|
||||
get ready() {
|
||||
return sync.ready
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -683,6 +683,7 @@ export function Session() {
|
||||
<scrollbox
|
||||
ref={(r) => (scroll = r)}
|
||||
scrollbarOptions={{
|
||||
paddingLeft: 2,
|
||||
trackOptions: {
|
||||
backgroundColor: theme.backgroundElement,
|
||||
foregroundColor: theme.border,
|
||||
|
||||
114
packages/opencode/src/cli/cmd/tui/util/terminal.ts
Normal file
114
packages/opencode/src/cli/cmd/tui/util/terminal.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { RGBA } from "@opentui/core"
|
||||
|
||||
export namespace Terminal {
|
||||
export type Colors = Awaited<ReturnType<typeof colors>>
|
||||
/**
|
||||
* Query terminal colors including background, foreground, and palette (0-15).
|
||||
* Uses OSC escape sequences to retrieve actual terminal color values.
|
||||
*
|
||||
* Note: OSC 4 (palette) queries may not work through tmux as responses are filtered.
|
||||
* OSC 10/11 (foreground/background) typically work in most environments.
|
||||
*
|
||||
* Returns an object with background, foreground, and colors array.
|
||||
* Any query that fails will be null/empty.
|
||||
*/
|
||||
export async function colors(): Promise<{
|
||||
background: RGBA | null
|
||||
foreground: RGBA | null
|
||||
colors: RGBA[]
|
||||
}> {
|
||||
if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] }
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let background: RGBA | null = null
|
||||
let foreground: RGBA | null = null
|
||||
const paletteColors: RGBA[] = []
|
||||
let timeout: NodeJS.Timeout
|
||||
|
||||
const cleanup = () => {
|
||||
process.stdin.setRawMode(false)
|
||||
process.stdin.removeListener("data", handler)
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
const parseColor = (colorStr: string): RGBA | null => {
|
||||
if (colorStr.startsWith("rgb:")) {
|
||||
const parts = colorStr.substring(4).split("/")
|
||||
return RGBA.fromInts(
|
||||
parseInt(parts[0], 16) >> 8, // Convert 16-bit to 8-bit
|
||||
parseInt(parts[1], 16) >> 8,
|
||||
parseInt(parts[2], 16) >> 8,
|
||||
255,
|
||||
)
|
||||
}
|
||||
if (colorStr.startsWith("#")) {
|
||||
return RGBA.fromHex(colorStr)
|
||||
}
|
||||
if (colorStr.startsWith("rgb(")) {
|
||||
const parts = colorStr.substring(4, colorStr.length - 1).split(",")
|
||||
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const handler = (data: Buffer) => {
|
||||
const str = data.toString()
|
||||
|
||||
// Match OSC 11 (background color)
|
||||
const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
||||
if (bgMatch) {
|
||||
background = parseColor(bgMatch[1])
|
||||
}
|
||||
|
||||
// Match OSC 10 (foreground color)
|
||||
const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
|
||||
if (fgMatch) {
|
||||
foreground = parseColor(fgMatch[1])
|
||||
}
|
||||
|
||||
// Match OSC 4 (palette colors)
|
||||
const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
|
||||
for (const match of paletteMatches) {
|
||||
const index = parseInt(match[1])
|
||||
const color = parseColor(match[2])
|
||||
if (color) paletteColors[index] = color
|
||||
}
|
||||
|
||||
// Return immediately if we have all 16 palette colors
|
||||
if (paletteColors.filter((c) => c !== undefined).length === 16) {
|
||||
cleanup()
|
||||
resolve({ background, foreground, colors: paletteColors })
|
||||
}
|
||||
}
|
||||
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.on("data", handler)
|
||||
|
||||
// Query background (OSC 11)
|
||||
process.stdout.write("\x1b]11;?\x07")
|
||||
// Query foreground (OSC 10)
|
||||
process.stdout.write("\x1b]10;?\x07")
|
||||
// Query palette colors 0-15 (OSC 4)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
process.stdout.write(`\x1b]4;${i};?\x07`)
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
resolve({ background, foreground, colors: paletteColors })
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
const result = await colors()
|
||||
if (!result.background) return "dark"
|
||||
|
||||
const { r, g, b } = result.background
|
||||
// Calculate luminance using relative luminance formula
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
|
||||
// Determine if dark or light based on luminance threshold
|
||||
return luminance > 0.5 ? "light" : "dark"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user