mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-22 13:52:34 +08:00
fix(cli): use terminal accents for system theme primary
This commit is contained in:
@@ -513,6 +513,32 @@ export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
||||
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
||||
}
|
||||
|
||||
function luminance(color: RGBA) {
|
||||
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b
|
||||
}
|
||||
|
||||
function chroma(color: RGBA) {
|
||||
return Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b)
|
||||
}
|
||||
|
||||
function pickPrimaryColor(
|
||||
bg: RGBA,
|
||||
candidates: Array<{
|
||||
key: string
|
||||
color: RGBA | undefined
|
||||
}>,
|
||||
) {
|
||||
return candidates
|
||||
.flatMap((item) => {
|
||||
if (!item.color) return []
|
||||
const contrast = Math.abs(luminance(item.color) - luminance(bg))
|
||||
const vivid = chroma(item.color)
|
||||
if (contrast < 0.16 || vivid < 0.12) return []
|
||||
return [{ ...item, score: vivid * 1.5 + contrast }]
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)[0]
|
||||
}
|
||||
|
||||
// TODO: i exported this, just for keeping it simple for now, but this should
|
||||
// probably go into something shared if we decide to use this in opencode run
|
||||
export function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
||||
@@ -552,13 +578,37 @@ export function generateSystem(colors: TerminalColors, mode: "dark" | "light"):
|
||||
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
||||
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
||||
const diffLineNumber = textMuted
|
||||
// The generated system theme also feeds the run footer highlight, so prefer
|
||||
// the terminal's own cursor/selection accent when it stays legible.
|
||||
const primary =
|
||||
pickPrimaryColor(bg, [
|
||||
{
|
||||
key: "cursor",
|
||||
color: colors.cursorColor ? RGBA.fromHex(colors.cursorColor) : undefined,
|
||||
},
|
||||
{
|
||||
key: "selection",
|
||||
color: colors.highlightBackground ? RGBA.fromHex(colors.highlightBackground) : undefined,
|
||||
},
|
||||
{
|
||||
key: "blue",
|
||||
color: ansiColors.blue,
|
||||
},
|
||||
{
|
||||
key: "magenta",
|
||||
color: ansiColors.magenta,
|
||||
},
|
||||
]) ?? {
|
||||
key: "blue",
|
||||
color: ansiColors.blue,
|
||||
}
|
||||
|
||||
return {
|
||||
theme: {
|
||||
// Primary colors using ANSI
|
||||
primary: ansiColors.cyan,
|
||||
secondary: ansiColors.magenta,
|
||||
accent: ansiColors.cyan,
|
||||
// Fall back to blue/magenta when the terminal UI colors are too muted.
|
||||
primary: primary.color,
|
||||
secondary: primary.key === "magenta" ? ansiColors.blue : ansiColors.magenta,
|
||||
accent: primary.color,
|
||||
|
||||
// Status colors using ANSI
|
||||
error: ansiColors.red,
|
||||
|
||||
@@ -1,29 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { RGBA, SyntaxStyle } from "@opentui/core"
|
||||
import { opaqueSyntaxStyle } from "@/cli/cmd/run/theme"
|
||||
import { expect, test } from "bun:test"
|
||||
import { RGBA, SyntaxStyle, type CliRenderer, type TerminalColors } from "@opentui/core"
|
||||
import { opaqueSyntaxStyle, resolveRunTheme } from "@/cli/cmd/run/theme"
|
||||
import { generateSystem, resolveTheme } from "@/cli/cmd/tui/context/theme"
|
||||
|
||||
describe("run theme", () => {
|
||||
test("flattens subtle syntax alpha against the run background", () => {
|
||||
const syntax = SyntaxStyle.fromStyles({
|
||||
default: {
|
||||
fg: RGBA.fromInts(169, 177, 214, 153),
|
||||
},
|
||||
emphasis: {
|
||||
fg: RGBA.fromInts(224, 175, 104, 153),
|
||||
italic: true,
|
||||
bold: true,
|
||||
},
|
||||
})
|
||||
const subtle = opaqueSyntaxStyle(syntax, RGBA.fromInts(42, 43, 61))
|
||||
|
||||
try {
|
||||
expect(subtle?.getStyle("default")?.fg?.toInts()).toEqual([118, 123, 153, 255])
|
||||
expect(subtle?.getStyle("emphasis")?.fg?.toInts()).toEqual([151, 122, 87, 255])
|
||||
expect(subtle?.getStyle("emphasis")?.italic).toBe(true)
|
||||
expect(subtle?.getStyle("emphasis")?.bold).toBe(true)
|
||||
} finally {
|
||||
syntax.destroy()
|
||||
subtle?.destroy()
|
||||
}
|
||||
test("flattens subtle syntax alpha against the run background", () => {
|
||||
const syntax = SyntaxStyle.fromStyles({
|
||||
default: {
|
||||
fg: RGBA.fromInts(169, 177, 214, 153),
|
||||
},
|
||||
emphasis: {
|
||||
fg: RGBA.fromInts(224, 175, 104, 153),
|
||||
italic: true,
|
||||
bold: true,
|
||||
},
|
||||
})
|
||||
const subtle = opaqueSyntaxStyle(syntax, RGBA.fromInts(42, 43, 61))
|
||||
|
||||
try {
|
||||
expect(subtle?.getStyle("default")?.fg?.toInts()).toEqual([118, 123, 153, 255])
|
||||
expect(subtle?.getStyle("emphasis")?.fg?.toInts()).toEqual([151, 122, 87, 255])
|
||||
expect(subtle?.getStyle("emphasis")?.italic).toBe(true)
|
||||
expect(subtle?.getStyle("emphasis")?.bold).toBe(true)
|
||||
} finally {
|
||||
syntax.destroy()
|
||||
subtle?.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
const colors: TerminalColors = {
|
||||
palette: [
|
||||
"#15161e",
|
||||
"#f7768e",
|
||||
"#9ece6a",
|
||||
"#e0af68",
|
||||
"#7aa2f7",
|
||||
"#bb9af7",
|
||||
"#7dcfff",
|
||||
"#a9b1d6",
|
||||
"#414868",
|
||||
"#f7768e",
|
||||
"#9ece6a",
|
||||
"#e0af68",
|
||||
"#7aa2f7",
|
||||
"#bb9af7",
|
||||
"#7dcfff",
|
||||
"#c0caf5",
|
||||
],
|
||||
defaultBackground: "#1a1b26",
|
||||
defaultForeground: "#c0caf5",
|
||||
cursorColor: "#ff9e64",
|
||||
mouseForeground: null,
|
||||
mouseBackground: null,
|
||||
tekForeground: null,
|
||||
tekBackground: null,
|
||||
highlightBackground: "#33467c",
|
||||
highlightForeground: "#c0caf5",
|
||||
}
|
||||
|
||||
function renderer(themeMode: "dark" | "light") {
|
||||
const item = {
|
||||
themeMode,
|
||||
getPalette: async () => colors,
|
||||
} satisfies Pick<CliRenderer, "themeMode" | "getPalette">
|
||||
|
||||
return item as CliRenderer
|
||||
}
|
||||
|
||||
test("system theme uses terminal ui colors for primary", () => {
|
||||
const theme = resolveTheme(generateSystem(colors, "dark"), "dark")
|
||||
|
||||
expect(theme.primary).toEqual(RGBA.fromHex(colors.cursorColor!))
|
||||
expect(theme.primary).not.toEqual(RGBA.fromHex(colors.palette[6]!))
|
||||
})
|
||||
|
||||
test("resolve run theme uses the system primary for footer highlight", async () => {
|
||||
const expected = resolveTheme(generateSystem(colors, "dark"), "dark")
|
||||
const theme = await resolveRunTheme(renderer("dark"))
|
||||
|
||||
expect(theme.footer.highlight).toEqual(expected.primary)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user