mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 09:10:47 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 212d6797bc |
@@ -16,7 +16,8 @@ import {
|
|||||||
type Theme,
|
type Theme,
|
||||||
type ThemeJson,
|
type ThemeJson,
|
||||||
} from "../theme"
|
} from "../theme"
|
||||||
import { generateSystem, terminalMode } from "../theme/system"
|
import { generateSystem, generateSystemV2, terminalMode } from "../theme/system"
|
||||||
|
import type { ThemeFile } from "../theme/v2"
|
||||||
import { createComponentTheme, type ComponentTheme } from "../theme/v2/component"
|
import { createComponentTheme, type ComponentTheme } from "../theme/v2/component"
|
||||||
import { resolveThemeFile } from "../theme/v2/resolve"
|
import { resolveThemeFile } from "../theme/v2/resolve"
|
||||||
import { migrateV1 } from "../theme/v2/v1-migrate"
|
import { migrateV1 } from "../theme/v2/v1-migrate"
|
||||||
@@ -78,6 +79,7 @@ const THEME_REFRESH_DELAYS = [250, 1000] as const
|
|||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
themes: Record<string, ThemeJson>
|
themes: Record<string, ThemeJson>
|
||||||
|
systemV2: ThemeFile | undefined
|
||||||
mode: "dark" | "light"
|
mode: "dark" | "light"
|
||||||
lock: "dark" | "light" | undefined
|
lock: "dark" | "light" | undefined
|
||||||
active: string
|
active: string
|
||||||
@@ -104,6 +106,7 @@ type ThemeService = {
|
|||||||
|
|
||||||
const [store, setStore] = createStore<State>({
|
const [store, setStore] = createStore<State>({
|
||||||
themes: allThemes(),
|
themes: allThemes(),
|
||||||
|
systemV2: undefined,
|
||||||
mode: "dark",
|
mode: "dark",
|
||||||
lock: undefined,
|
lock: undefined,
|
||||||
active: "opencode",
|
active: "opencode",
|
||||||
@@ -181,6 +184,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
.then((colors: TerminalColors) => {
|
.then((colors: TerminalColors) => {
|
||||||
if (!colors.palette[0]) {
|
if (!colors.palette[0]) {
|
||||||
if (hasResolvedSystemTheme) return
|
if (hasResolvedSystemTheme) return
|
||||||
|
setStore("systemV2", undefined)
|
||||||
setSystemTheme(undefined)
|
setSystemTheme(undefined)
|
||||||
if (store.active === "system") setStore("active", "opencode")
|
if (store.active === "system") setStore("active", "opencode")
|
||||||
return
|
return
|
||||||
@@ -192,10 +196,12 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
if (store.themes.system && systemThemeSignature === signature && systemThemeMode === next) return
|
if (store.themes.system && systemThemeSignature === signature && systemThemeMode === next) return
|
||||||
systemThemeSignature = signature
|
systemThemeSignature = signature
|
||||||
systemThemeMode = next
|
systemThemeMode = next
|
||||||
|
setStore("systemV2", generateSystemV2(colors))
|
||||||
setSystemTheme(generateSystem(colors, next))
|
setSystemTheme(generateSystem(colors, next))
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (hasResolvedSystemTheme) return
|
if (hasResolvedSystemTheme) return
|
||||||
|
setStore("systemV2", undefined)
|
||||||
setSystemTheme(undefined)
|
setSystemTheme(undefined)
|
||||||
if (store.active === "system") setStore("active", "opencode")
|
if (store.active === "system") setStore("active", "opencode")
|
||||||
})
|
})
|
||||||
@@ -290,7 +296,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
const values = createMemo(() => resolveTheme(source(), store.mode))
|
const values = createMemo(() => resolveTheme(source(), store.mode))
|
||||||
const valuesV2 = createMemo(() => {
|
const valuesV2 = createMemo(() => {
|
||||||
const started = performance.now()
|
const started = performance.now()
|
||||||
const file = migrateV1(source())
|
const file = sourceName() === "system" && store.systemV2 ? store.systemV2 : migrateV1(source())
|
||||||
themePerformance.set("Convert V1 to V2", duration(performance.now() - started))
|
themePerformance.set("Convert V1 to V2", duration(performance.now() - started))
|
||||||
const resolveStarted = performance.now()
|
const resolveStarted = performance.now()
|
||||||
const result = resolveThemeFile(file, store.mode, sourceName())
|
const result = resolveThemeFile(file, store.mode, sourceName())
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RGBA, type TerminalColors } from "@opentui/core"
|
import { RGBA, type TerminalColors } from "@opentui/core"
|
||||||
import { ansiToRgba, tint } from "./color"
|
import { ansiToRgba, tint } from "./color"
|
||||||
|
import { HueStep, type Mode, type ThemeFile } from "./v2"
|
||||||
|
|
||||||
export function terminalMode(colors: TerminalColors): "dark" | "light" | undefined {
|
export function terminalMode(colors: TerminalColors): "dark" | "light" | undefined {
|
||||||
const bg = colors.defaultBackground
|
const bg = colors.defaultBackground
|
||||||
@@ -9,6 +10,97 @@ export function terminalMode(colors: TerminalColors): "dark" | "light" | undefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateSystem(colors: TerminalColors, mode: "dark" | "light") {
|
export function generateSystem(colors: TerminalColors, mode: "dark" | "light") {
|
||||||
|
const value = resolveSystem(colors, mode)
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme: {
|
||||||
|
primary: value.ansi.cyan,
|
||||||
|
secondary: value.ansi.magenta,
|
||||||
|
accent: value.ansi.cyan,
|
||||||
|
error: value.ansi.red,
|
||||||
|
warning: value.ansi.yellow,
|
||||||
|
success: value.ansi.green,
|
||||||
|
info: value.ansi.cyan,
|
||||||
|
text: value.fg,
|
||||||
|
textMuted: value.textMuted,
|
||||||
|
selectedListItemText: value.bg,
|
||||||
|
background: value.transparent,
|
||||||
|
backgroundPanel: value.grays[2],
|
||||||
|
backgroundElement: value.grays[3],
|
||||||
|
backgroundMenu: value.grays[3],
|
||||||
|
borderSubtle: value.grays[6],
|
||||||
|
border: value.grays[7],
|
||||||
|
borderActive: value.grays[8],
|
||||||
|
diffAdded: value.ansi.green,
|
||||||
|
diffRemoved: value.ansi.red,
|
||||||
|
diffContext: value.grays[7],
|
||||||
|
diffHunkHeader: value.grays[7],
|
||||||
|
diffHighlightAdded: value.ansi.greenBright,
|
||||||
|
diffHighlightRemoved: value.ansi.redBright,
|
||||||
|
diffAddedBg: value.diffAddedBg,
|
||||||
|
diffRemovedBg: value.diffRemovedBg,
|
||||||
|
diffContextBg: value.diffContextBg,
|
||||||
|
diffLineNumber: value.textMuted,
|
||||||
|
diffAddedLineNumberBg: value.diffAddedLineNumberBg,
|
||||||
|
diffRemovedLineNumberBg: value.diffRemovedLineNumberBg,
|
||||||
|
markdownText: value.fg,
|
||||||
|
markdownHeading: value.fg,
|
||||||
|
markdownLink: value.ansi.blue,
|
||||||
|
markdownLinkText: value.ansi.cyan,
|
||||||
|
markdownCode: value.ansi.green,
|
||||||
|
markdownBlockQuote: value.ansi.yellow,
|
||||||
|
markdownEmph: value.ansi.yellow,
|
||||||
|
markdownStrong: value.fg,
|
||||||
|
markdownHorizontalRule: value.grays[7],
|
||||||
|
markdownListItem: value.ansi.blue,
|
||||||
|
markdownListEnumeration: value.ansi.cyan,
|
||||||
|
markdownImage: value.ansi.blue,
|
||||||
|
markdownImageText: value.ansi.cyan,
|
||||||
|
markdownCodeBlock: value.fg,
|
||||||
|
syntaxComment: value.textMuted,
|
||||||
|
syntaxKeyword: value.ansi.magenta,
|
||||||
|
syntaxFunction: value.ansi.blue,
|
||||||
|
syntaxVariable: value.fg,
|
||||||
|
syntaxString: value.ansi.green,
|
||||||
|
syntaxNumber: value.ansi.yellow,
|
||||||
|
syntaxType: value.ansi.cyan,
|
||||||
|
syntaxOperator: value.ansi.cyan,
|
||||||
|
syntaxPunctuation: value.fg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSystemV2(colors: TerminalColors): ThemeFile {
|
||||||
|
return {
|
||||||
|
version: 2,
|
||||||
|
light: generateSystemMode(colors, "light"),
|
||||||
|
dark: generateSystemMode(colors, "dark"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSystemMode(colors: TerminalColors, mode: Mode): ThemeFile["light"] {
|
||||||
|
const value = resolveSystem(colors, mode)
|
||||||
|
const scale = (color: RGBA) =>
|
||||||
|
Object.fromEntries(HueStep.literals.map((step) => [step, hex(color)])) as Record<HueStep, string>
|
||||||
|
|
||||||
|
return {
|
||||||
|
hue: {
|
||||||
|
gray: neutralScale(value, mode),
|
||||||
|
red: scale(value.ansi.red),
|
||||||
|
orange: scale(value.ansi.yellow),
|
||||||
|
yellow: scale(value.ansi.yellow),
|
||||||
|
green: scale(value.ansi.green),
|
||||||
|
cyan: scale(value.ansi.cyan),
|
||||||
|
blue: scale(value.ansi.blue),
|
||||||
|
purple: scale(value.ansi.magenta),
|
||||||
|
accent: scale(value.ansi.cyan),
|
||||||
|
interactive: scale(value.ansi.cyan),
|
||||||
|
neutral: "$hue.gray",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSystem(colors: TerminalColors, mode: Mode) {
|
||||||
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||||
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||||
const transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0)
|
const transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0)
|
||||||
@@ -22,7 +114,7 @@ export function generateSystem(colors: TerminalColors, mode: "dark" | "light") {
|
|||||||
|
|
||||||
const grays = generateGrayScale(bg, isDark)
|
const grays = generateGrayScale(bg, isDark)
|
||||||
const textMuted = generateMutedTextColor(bg, isDark)
|
const textMuted = generateMutedTextColor(bg, isDark)
|
||||||
const ansiColors = {
|
const ansi = {
|
||||||
red: col(1),
|
red: col(1),
|
||||||
green: col(2),
|
green: col(2),
|
||||||
yellow: col(3),
|
yellow: col(3),
|
||||||
@@ -34,67 +126,24 @@ export function generateSystem(colors: TerminalColors, mode: "dark" | "light") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const diffAlpha = isDark ? 0.22 : 0.14
|
const diffAlpha = isDark ? 0.22 : 0.14
|
||||||
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
const diffAddedBg = tint(bg, ansi.green, diffAlpha)
|
||||||
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
const diffRemovedBg = tint(bg, ansi.red, diffAlpha)
|
||||||
const diffContextBg = grays[2]
|
const diffContextBg = grays[2]
|
||||||
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
const diffAddedLineNumberBg = tint(diffContextBg, ansi.green, diffAlpha)
|
||||||
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
const diffRemovedLineNumberBg = tint(diffContextBg, ansi.red, diffAlpha)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: {
|
bg,
|
||||||
primary: ansiColors.cyan,
|
fg,
|
||||||
secondary: ansiColors.magenta,
|
transparent,
|
||||||
accent: ansiColors.cyan,
|
grays,
|
||||||
error: ansiColors.red,
|
textMuted,
|
||||||
warning: ansiColors.yellow,
|
ansi,
|
||||||
success: ansiColors.green,
|
diffAddedBg,
|
||||||
info: ansiColors.cyan,
|
diffRemovedBg,
|
||||||
text: fg,
|
diffContextBg,
|
||||||
textMuted,
|
diffAddedLineNumberBg,
|
||||||
selectedListItemText: bg,
|
diffRemovedLineNumberBg,
|
||||||
background: transparent,
|
|
||||||
backgroundPanel: grays[2],
|
|
||||||
backgroundElement: grays[3],
|
|
||||||
backgroundMenu: grays[3],
|
|
||||||
borderSubtle: grays[6],
|
|
||||||
border: grays[7],
|
|
||||||
borderActive: grays[8],
|
|
||||||
diffAdded: ansiColors.green,
|
|
||||||
diffRemoved: ansiColors.red,
|
|
||||||
diffContext: grays[7],
|
|
||||||
diffHunkHeader: grays[7],
|
|
||||||
diffHighlightAdded: ansiColors.greenBright,
|
|
||||||
diffHighlightRemoved: ansiColors.redBright,
|
|
||||||
diffAddedBg,
|
|
||||||
diffRemovedBg,
|
|
||||||
diffContextBg,
|
|
||||||
diffLineNumber: textMuted,
|
|
||||||
diffAddedLineNumberBg,
|
|
||||||
diffRemovedLineNumberBg,
|
|
||||||
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,
|
|
||||||
syntaxComment: textMuted,
|
|
||||||
syntaxKeyword: ansiColors.magenta,
|
|
||||||
syntaxFunction: ansiColors.blue,
|
|
||||||
syntaxVariable: fg,
|
|
||||||
syntaxString: ansiColors.green,
|
|
||||||
syntaxNumber: ansiColors.yellow,
|
|
||||||
syntaxType: ansiColors.cyan,
|
|
||||||
syntaxOperator: ansiColors.cyan,
|
|
||||||
syntaxPunctuation: fg,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,3 +191,41 @@ function generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA {
|
|||||||
const gray = luminance > 245 ? 75 : Math.max(Math.floor(100 - (255 - luminance) * 0.2), 60)
|
const gray = luminance > 245 ? 75 : Math.max(Math.floor(100 - (255 - luminance) * 0.2), 60)
|
||||||
return RGBA.fromInts(gray, gray, gray)
|
return RGBA.fromInts(gray, gray, gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function neutralScale(value: ReturnType<typeof resolveSystem>, mode: Mode) {
|
||||||
|
const light: { step: HueStep; color: RGBA }[] = [
|
||||||
|
{ step: 100, color: value.transparent },
|
||||||
|
{ step: 200, color: value.grays[2] },
|
||||||
|
{ step: 300, color: value.grays[3] },
|
||||||
|
{ step: 700, color: value.textMuted },
|
||||||
|
{ step: 900, color: value.fg },
|
||||||
|
]
|
||||||
|
const anchors =
|
||||||
|
mode === "light"
|
||||||
|
? light
|
||||||
|
: light.toReversed().map((source) => ({ ...source, step: (1000 - source.step) as HueStep }))
|
||||||
|
return Object.fromEntries(
|
||||||
|
HueStep.literals.map((step) => {
|
||||||
|
const exact = anchors.find((anchor) => anchor.step === step)
|
||||||
|
if (exact) return [step, hex(exact.color)]
|
||||||
|
const lower = anchors.filter((anchor) => anchor.step < step).at(-1)!
|
||||||
|
const upper = anchors.find((anchor) => anchor.step > step)!
|
||||||
|
return [step, interpolate(lower.color, upper.color, (step - lower.step) / (upper.step - lower.step))]
|
||||||
|
}),
|
||||||
|
) as Record<HueStep, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolate(first: RGBA, second: RGBA, amount: number) {
|
||||||
|
const start = first.toInts()
|
||||||
|
const end = second.toInts()
|
||||||
|
return `#${start.map((value, index) => byte(Math.round(value + (end[index]! - value) * amount))).join("")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex(color: RGBA) {
|
||||||
|
const [red, green, blue, alpha] = color.toInts()
|
||||||
|
return `#${byte(red)}${byte(green)}${byte(blue)}${alpha === 255 ? "" : byte(alpha)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function byte(value: number) {
|
||||||
|
return value.toString(16).padStart(2, "0")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import type { TerminalColors } from "@opentui/core"
|
||||||
|
import { generateSystemV2 } from "../../src/theme/system"
|
||||||
|
import { resolveThemeFile } from "../../src/theme/v2/resolve"
|
||||||
|
|
||||||
|
const colors: TerminalColors = {
|
||||||
|
palette: ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6"],
|
||||||
|
defaultForeground: "#c0caf5",
|
||||||
|
defaultBackground: "#1a1b26",
|
||||||
|
cursorColor: null,
|
||||||
|
mouseForeground: null,
|
||||||
|
mouseBackground: null,
|
||||||
|
tekForeground: null,
|
||||||
|
tekBackground: null,
|
||||||
|
highlightBackground: null,
|
||||||
|
highlightForeground: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
test("generates literal V2 hue scales from the system palette", () => {
|
||||||
|
const resolved = resolveThemeFile(generateSystemV2(colors), "dark")
|
||||||
|
|
||||||
|
for (const step of [100, 300, 500, 700, 900] as const) {
|
||||||
|
expect(resolved.hue.red[step].toInts()).toEqual([247, 118, 142, 255])
|
||||||
|
expect(resolved.hue.orange[step].toInts()).toEqual([224, 175, 104, 255])
|
||||||
|
expect(resolved.hue.yellow[step].toInts()).toEqual([224, 175, 104, 255])
|
||||||
|
expect(resolved.hue.green[step].toInts()).toEqual([158, 206, 106, 255])
|
||||||
|
expect(resolved.hue.cyan[step].toInts()).toEqual([125, 207, 255, 255])
|
||||||
|
expect(resolved.hue.blue[step].toInts()).toEqual([122, 162, 247, 255])
|
||||||
|
expect(resolved.hue.purple[step].toInts()).toEqual([187, 154, 247, 255])
|
||||||
|
expect(resolved.hue.accent[step].toInts()).toEqual([125, 207, 255, 255])
|
||||||
|
expect(resolved.hue.interactive[step].toInts()).toEqual([125, 207, 255, 255])
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(resolved.text.default.toInts()).toEqual([192, 202, 245, 255])
|
||||||
|
expect(resolved.background.default.toInts()).toEqual([26, 27, 38, 0])
|
||||||
|
expect(resolved.background.action.primary.default.toInts()).toEqual([125, 207, 255, 255])
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user