mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(tui): derive bright terminal palette (#50433)
This commit is contained in:
@@ -43,6 +43,7 @@ export type {
|
||||
StatefulColor,
|
||||
} from "./types.js"
|
||||
export { DEFAULT_CATEGORICAL } from "./categorical.js"
|
||||
export { rgbToOklch } from "./color.js"
|
||||
export { expandTheme } from "./expand.js"
|
||||
export { migrateV1 } from "./v1-migrate.js"
|
||||
export { resolveTheme, resolveThemeDocument, themeDecodeError } from "./resolve.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EmbeddedTerminalRenderable, type RGBA } from "@opentui/core"
|
||||
import type { ResolvedThemeTokens } from "@opencode/theme/tui"
|
||||
import { rgbToOklch, type ResolvedThemeTokens } from "@opencode/theme/tui"
|
||||
import { extend, useRenderer } from "@opentui/solid"
|
||||
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useClient } from "../context/client"
|
||||
@@ -156,7 +156,7 @@ export function TerminalPane(props: {
|
||||
|
||||
createEffect(() => {
|
||||
const tokens = themes.currentTokens()
|
||||
terminalTheme = terminalPalette(tokens, tokens.background.raised.base)
|
||||
terminalTheme = terminalPalette(tokens, themes.mode(), tokens.background.raised.base)
|
||||
applyTerminalTheme()
|
||||
})
|
||||
|
||||
@@ -335,26 +335,27 @@ function sameSize(first: TerminalSize | undefined, second: TerminalSize | undefi
|
||||
return !!first && !!second && first.cols === second.cols && first.rows === second.rows
|
||||
}
|
||||
|
||||
function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
|
||||
const base = 200
|
||||
const bright = 100
|
||||
const colors = [
|
||||
background,
|
||||
export function terminalPalette(theme: ResolvedThemeTokens, mode: "light" | "dark", background: RGBA) {
|
||||
const black = theme.hue.neutral[mode === "dark" ? 800 : 200]
|
||||
const white = theme.hue.neutral[mode === "dark" ? 200 : 800]
|
||||
const brightWhite = theme.hue.neutral[mode === "dark" ? 100 : 900]
|
||||
const blue = terminalHue(theme, rgbToOklch(0, 0, 1).h)
|
||||
const magenta = terminalHue(theme, rgbToOklch(1, 0, 1).h)
|
||||
const normal = [
|
||||
theme.text.feedback.error.base,
|
||||
theme.text.feedback.success.base,
|
||||
theme.text.feedback.warning.base,
|
||||
theme.hue.blue[base],
|
||||
theme.hue.purple[base],
|
||||
blue,
|
||||
magenta,
|
||||
theme.text.feedback.info.base,
|
||||
theme.text.base,
|
||||
white,
|
||||
]
|
||||
const colors = [
|
||||
black,
|
||||
...normal,
|
||||
theme.text.muted,
|
||||
theme.text.feedback.error.muted,
|
||||
theme.text.feedback.success.muted,
|
||||
theme.text.feedback.warning.muted,
|
||||
theme.hue.blue[bright],
|
||||
theme.hue.purple[bright],
|
||||
theme.hue.cyan[bright],
|
||||
theme.hue.neutral[100],
|
||||
...normal.slice(0, -1).map((color) => theme.decrease(color)),
|
||||
brightWhite,
|
||||
]
|
||||
return Buffer.from(
|
||||
colors
|
||||
@@ -364,6 +365,25 @@ function terminalPalette(theme: ResolvedThemeTokens, background: RGBA) {
|
||||
)
|
||||
}
|
||||
|
||||
function terminalHue(theme: ResolvedThemeTokens, target: number) {
|
||||
const nearest = Object.values(theme.hue)
|
||||
.map((scale) => {
|
||||
const color = scale[200]
|
||||
const [red, green, blue, alpha] = color.toInts()
|
||||
const converted = rgbToOklch(red / 255, green / 255, blue / 255)
|
||||
return { color, alpha, chroma: converted.c, distance: hueDistance(converted.h, target) }
|
||||
})
|
||||
.filter((item) => item.alpha > 0 && item.chroma >= 0.03)
|
||||
.sort((first, second) => first.distance - second.distance)[0]
|
||||
if (!nearest || nearest.distance > 30) return theme.hue.accent[200]
|
||||
return nearest.color
|
||||
}
|
||||
|
||||
function hueDistance(first: number, second: number) {
|
||||
const difference = Math.abs(first - second)
|
||||
return Math.min(difference, 360 - difference)
|
||||
}
|
||||
|
||||
function hex(color: RGBA) {
|
||||
return `#${color
|
||||
.toInts()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { resolveThemeDocument } from "@opencode/theme/tui"
|
||||
import { terminalPalette } from "../../src/component/terminal-pane"
|
||||
import { getOpenCodeTheme } from "../../src/theme"
|
||||
|
||||
test.each(["light", "dark"] as const)("builds the %s ANSI palette from semantic colors", (mode) => {
|
||||
const theme = resolveThemeDocument(getOpenCodeTheme(), mode)
|
||||
const black = theme.hue.neutral[mode === "dark" ? 800 : 200]
|
||||
const white = theme.hue.neutral[mode === "dark" ? 200 : 800]
|
||||
const brightWhite = theme.hue.neutral[mode === "dark" ? 100 : 900]
|
||||
const normal = [
|
||||
theme.text.feedback.error.base,
|
||||
theme.text.feedback.success.base,
|
||||
theme.text.feedback.warning.base,
|
||||
theme.hue.blue[200],
|
||||
mode === "dark" ? theme.hue.purple[200] : theme.hue.accent[200],
|
||||
theme.text.feedback.info.base,
|
||||
white,
|
||||
]
|
||||
const expected = [
|
||||
black,
|
||||
...normal,
|
||||
theme.text.muted,
|
||||
...normal.slice(0, -1).map((color) => theme.decrease(color)),
|
||||
brightWhite,
|
||||
]
|
||||
const output = terminalPalette(theme, mode, theme.background.raised.base).toString()
|
||||
const palette = [...output.matchAll(/\]4;(\d+);(#[0-9a-f]+)/g)]
|
||||
|
||||
expect(palette).toHaveLength(16)
|
||||
expected.forEach((color, index) => {
|
||||
expect(palette[index]?.[1]).toBe(String(index))
|
||||
expect(palette[index]?.[2]).toBe(hex(color))
|
||||
})
|
||||
expect(output).toContain(`]10;${hex(theme.text.base)}`)
|
||||
expect(output).toContain(`]11;${hex(theme.background.raised.base)}`)
|
||||
})
|
||||
|
||||
test("falls back to accent when no hue is confidently blue or magenta", () => {
|
||||
const resolved = resolveThemeDocument(getOpenCodeTheme(), "dark")
|
||||
const hue = {
|
||||
...Object.fromEntries(Object.keys(resolved.hue).map((name) => [name, resolved.hue.red])),
|
||||
accent: resolved.hue.green,
|
||||
} as typeof resolved.hue
|
||||
const theme = { ...resolved, hue }
|
||||
const palette = [
|
||||
...terminalPalette(theme, "dark", theme.background.raised.base)
|
||||
.toString()
|
||||
.matchAll(/\]4;(\d+);(#[0-9a-f]+)/g),
|
||||
]
|
||||
|
||||
expect(palette[4]?.[2]).toBe(hex(theme.hue.accent[200]))
|
||||
expect(palette[5]?.[2]).toBe(hex(theme.hue.accent[200]))
|
||||
})
|
||||
|
||||
function byte(value: number) {
|
||||
return value.toString(16).padStart(2, "0")
|
||||
}
|
||||
|
||||
function hex(color: { toInts(): [number, number, number, number] }) {
|
||||
return `#${color.toInts().slice(0, 3).map(byte).join("")}`
|
||||
}
|
||||
Reference in New Issue
Block a user