refactor(tui): extract system theme generation (#36753)
This commit is contained in:
parent
748b0d8836
commit
4c45a5cf23
10 changed files with 206 additions and 268 deletions
|
|
@ -7,7 +7,8 @@ import {
|
||||||
} from "@opentui/core"
|
} from "@opentui/core"
|
||||||
import { extend, useRenderer } from "@opentui/solid"
|
import { extend, useRenderer } from "@opentui/solid"
|
||||||
import { onCleanup, onMount } from "solid-js"
|
import { onCleanup, onMount } from "solid-js"
|
||||||
import { tint, useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
|
import { tint } from "../theme/color"
|
||||||
import { GoUpsellArtPainter } from "./bg-pulse-render"
|
import { GoUpsellArtPainter } from "./bg-pulse-render"
|
||||||
|
|
||||||
type GoUpsellArtOptions = RenderableOptions<FrameBufferRenderable> & {
|
type GoUpsellArtOptions = RenderableOptions<FrameBufferRenderable> & {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { RGBA, TextAttributes } from "@opentui/core"
|
import { RGBA, TextAttributes } from "@opentui/core"
|
||||||
import { For, type JSX } from "solid-js"
|
import { For, type JSX } from "solid-js"
|
||||||
import { tint, useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
|
import { tint } from "../theme/color"
|
||||||
import { logo } from "../logo"
|
import { logo } from "../logo"
|
||||||
|
|
||||||
export function Logo() {
|
export function Logo() {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import path from "path"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { useLocal } from "../../context/local"
|
import { useLocal } from "../../context/local"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { tint, useTheme } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
|
import { tint } from "../../theme/color"
|
||||||
import { EmptyBorder, SplitBorder } from "../../ui/border"
|
import { EmptyBorder, SplitBorder } from "../../ui/border"
|
||||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||||
import { useClipboard } from "../../context/clipboard"
|
import { useClipboard } from "../../context/clipboard"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
addTheme,
|
addTheme,
|
||||||
allThemes,
|
allThemes,
|
||||||
generateSyntax,
|
generateSyntax,
|
||||||
generateSystem,
|
|
||||||
hasTheme,
|
hasTheme,
|
||||||
isTheme,
|
isTheme,
|
||||||
resolveTheme,
|
resolveTheme,
|
||||||
|
|
@ -13,11 +12,10 @@ import {
|
||||||
setCustomThemes,
|
setCustomThemes,
|
||||||
setSystemTheme,
|
setSystemTheme,
|
||||||
subscribeThemes,
|
subscribeThemes,
|
||||||
terminalMode,
|
|
||||||
tint,
|
|
||||||
upsertTheme,
|
upsertTheme,
|
||||||
type ThemeJson,
|
type ThemeJson,
|
||||||
} from "../theme"
|
} from "../theme"
|
||||||
|
import { generateSystem, terminalMode } from "../theme/system"
|
||||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
|
|
@ -63,13 +61,10 @@ export {
|
||||||
addTheme,
|
addTheme,
|
||||||
allThemes,
|
allThemes,
|
||||||
generateSyntax,
|
generateSyntax,
|
||||||
generateSystem,
|
|
||||||
hasTheme,
|
hasTheme,
|
||||||
isTheme,
|
isTheme,
|
||||||
resolveTheme,
|
resolveTheme,
|
||||||
selectedForeground,
|
selectedForeground,
|
||||||
terminalMode,
|
|
||||||
tint,
|
|
||||||
upsertTheme,
|
upsertTheme,
|
||||||
type Theme,
|
type Theme,
|
||||||
type ThemeJson,
|
type ThemeJson,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import type { ColorInput, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
import type { ColorInput, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
import { tint } from "../../context/theme"
|
import { tint } from "../../theme/color"
|
||||||
import { createEffect, createMemo, For, Match, Switch } from "solid-js"
|
import { createEffect, createMemo, For, Match, Switch } from "solid-js"
|
||||||
import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils"
|
import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils"
|
||||||
import { Panel } from "./diff-viewer-ui"
|
import { Panel } from "./diff-viewer-ui"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-j
|
||||||
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"
|
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { selectedForeground, tint, useTheme } from "../../context/theme"
|
import { selectedForeground, useTheme } from "../../context/theme"
|
||||||
|
import { tint } from "../../theme/color"
|
||||||
import type { FormField, FormValue } from "@opencode-ai/client"
|
import type { FormField, FormValue } from "@opencode-ai/client"
|
||||||
import type { FormWithLocation } from "../../context/data"
|
import type { FormWithLocation } from "../../context/data"
|
||||||
import { useSDK } from "../../context/sdk"
|
import { useSDK } from "../../context/sdk"
|
||||||
|
|
|
||||||
48
packages/tui/src/theme/color.ts
Normal file
48
packages/tui/src/theme/color.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { RGBA } from "@opentui/core"
|
||||||
|
|
||||||
|
export function ansiToRgba(code: number): RGBA {
|
||||||
|
if (code < 16) {
|
||||||
|
const ansiColors = [
|
||||||
|
"#000000",
|
||||||
|
"#800000",
|
||||||
|
"#008000",
|
||||||
|
"#808000",
|
||||||
|
"#000080",
|
||||||
|
"#800080",
|
||||||
|
"#008080",
|
||||||
|
"#c0c0c0",
|
||||||
|
"#808080",
|
||||||
|
"#ff0000",
|
||||||
|
"#00ff00",
|
||||||
|
"#ffff00",
|
||||||
|
"#0000ff",
|
||||||
|
"#ff00ff",
|
||||||
|
"#00ffff",
|
||||||
|
"#ffffff",
|
||||||
|
]
|
||||||
|
return RGBA.fromHex(ansiColors[code] ?? "#000000")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code < 232) {
|
||||||
|
const index = code - 16
|
||||||
|
const b = index % 6
|
||||||
|
const g = Math.floor(index / 6) % 6
|
||||||
|
const r = Math.floor(index / 36)
|
||||||
|
const val = (x: number) => (x === 0 ? 0 : x * 40 + 55)
|
||||||
|
return RGBA.fromInts(val(r), val(g), val(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code < 256) {
|
||||||
|
const gray = (code - 232) * 10 + 8
|
||||||
|
return RGBA.fromInts(gray, gray, gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
return RGBA.fromInts(0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
||||||
|
const r = base.r + (overlay.r - base.r) * alpha
|
||||||
|
const g = base.g + (overlay.g - base.g) * alpha
|
||||||
|
const b = base.b + (overlay.b - base.b) * alpha
|
||||||
|
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
import { SyntaxStyle, RGBA } from "@opentui/core"
|
||||||
|
import { ansiToRgba } from "./color"
|
||||||
import aura from "./assets/aura.json" with { type: "json" }
|
import aura from "./assets/aura.json" with { type: "json" }
|
||||||
import ayu from "./assets/ayu.json" with { type: "json" }
|
import ayu from "./assets/ayu.json" with { type: "json" }
|
||||||
import carbonfox from "./assets/carbonfox.json" with { type: "json" }
|
import carbonfox from "./assets/carbonfox.json" with { type: "json" }
|
||||||
|
|
@ -297,261 +298,6 @@ export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||||
} as Theme
|
} as Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
function ansiToRgba(code: number): RGBA {
|
|
||||||
// Standard ANSI colors (0-15)
|
|
||||||
if (code < 16) {
|
|
||||||
const ansiColors = [
|
|
||||||
"#000000", // Black
|
|
||||||
"#800000", // Red
|
|
||||||
"#008000", // Green
|
|
||||||
"#808000", // Yellow
|
|
||||||
"#000080", // Blue
|
|
||||||
"#800080", // Magenta
|
|
||||||
"#008080", // Cyan
|
|
||||||
"#c0c0c0", // White
|
|
||||||
"#808080", // Bright Black
|
|
||||||
"#ff0000", // Bright Red
|
|
||||||
"#00ff00", // Bright Green
|
|
||||||
"#ffff00", // Bright Yellow
|
|
||||||
"#0000ff", // Bright Blue
|
|
||||||
"#ff00ff", // Bright Magenta
|
|
||||||
"#00ffff", // Bright Cyan
|
|
||||||
"#ffffff", // Bright White
|
|
||||||
]
|
|
||||||
return RGBA.fromHex(ansiColors[code] ?? "#000000")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6x6x6 Color Cube (16-231)
|
|
||||||
if (code < 232) {
|
|
||||||
const index = code - 16
|
|
||||||
const b = index % 6
|
|
||||||
const g = Math.floor(index / 6) % 6
|
|
||||||
const r = Math.floor(index / 36)
|
|
||||||
|
|
||||||
const val = (x: number) => (x === 0 ? 0 : x * 40 + 55)
|
|
||||||
return RGBA.fromInts(val(r), val(g), val(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grayscale Ramp (232-255)
|
|
||||||
if (code < 256) {
|
|
||||||
const gray = (code - 232) * 10 + 8
|
|
||||||
return RGBA.fromInts(gray, gray, gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for invalid codes
|
|
||||||
return RGBA.fromInts(0, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
|
||||||
const r = base.r + (overlay.r - base.r) * alpha
|
|
||||||
const g = base.g + (overlay.g - base.g) * alpha
|
|
||||||
const b = base.b + (overlay.b - base.b) * alpha
|
|
||||||
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function terminalMode(colors: TerminalColors): "dark" | "light" | undefined {
|
|
||||||
const bg = colors.defaultBackground
|
|
||||||
if (!bg) return
|
|
||||||
const { r, g, b } = RGBA.fromHex(bg)
|
|
||||||
return 0.299 * r + 0.587 * g + 0.114 * b > 0.5 ? "light" : "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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 transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0)
|
|
||||||
const isDark = mode == "dark"
|
|
||||||
|
|
||||||
const col = (i: number) => {
|
|
||||||
const value = colors.palette[i]
|
|
||||||
if (value) return RGBA.fromHex(value)
|
|
||||||
return ansiToRgba(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate gray scale based on terminal background
|
|
||||||
const grays = generateGrayScale(bg, isDark)
|
|
||||||
const textMuted = generateMutedTextColor(bg, isDark)
|
|
||||||
|
|
||||||
// ANSI color references
|
|
||||||
const ansiColors = {
|
|
||||||
black: col(0),
|
|
||||||
red: col(1),
|
|
||||||
green: col(2),
|
|
||||||
yellow: col(3),
|
|
||||||
blue: col(4),
|
|
||||||
magenta: col(5),
|
|
||||||
cyan: col(6),
|
|
||||||
white: col(7),
|
|
||||||
redBright: col(9),
|
|
||||||
greenBright: col(10),
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffAlpha = isDark ? 0.22 : 0.14
|
|
||||||
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
|
||||||
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
|
||||||
const diffContextBg = grays[2]
|
|
||||||
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
|
||||||
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
|
||||||
const diffLineNumber = textMuted
|
|
||||||
|
|
||||||
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,
|
|
||||||
selectedListItemText: bg,
|
|
||||||
|
|
||||||
// Background colors - use transparent to respect terminal transparency
|
|
||||||
background: transparent,
|
|
||||||
backgroundPanel: grays[2],
|
|
||||||
backgroundElement: grays[3],
|
|
||||||
backgroundMenu: 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.greenBright,
|
|
||||||
diffHighlightRemoved: ansiColors.redBright,
|
|
||||||
diffAddedBg,
|
|
||||||
diffRemovedBg,
|
|
||||||
diffContextBg,
|
|
||||||
diffLineNumber,
|
|
||||||
diffAddedLineNumberBg,
|
|
||||||
diffRemovedLineNumberBg,
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateSyntax(theme: Theme) {
|
export function generateSyntax(theme: Theme) {
|
||||||
return SyntaxStyle.fromTheme(getSyntaxRules(theme))
|
return SyntaxStyle.fromTheme(getSyntaxRules(theme))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
packages/tui/src/theme/system.ts
Normal file
144
packages/tui/src/theme/system.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { RGBA, type TerminalColors } from "@opentui/core"
|
||||||
|
import { ansiToRgba, tint } from "./color"
|
||||||
|
|
||||||
|
export function terminalMode(colors: TerminalColors): "dark" | "light" | undefined {
|
||||||
|
const bg = colors.defaultBackground
|
||||||
|
if (!bg) return
|
||||||
|
const { r, g, b } = RGBA.fromHex(bg)
|
||||||
|
return 0.299 * r + 0.587 * g + 0.114 * b > 0.5 ? "light" : "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSystem(colors: TerminalColors, mode: "dark" | "light") {
|
||||||
|
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||||
|
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||||
|
const transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0)
|
||||||
|
const isDark = mode === "dark"
|
||||||
|
|
||||||
|
const col = (index: number) => {
|
||||||
|
const value = colors.palette[index]
|
||||||
|
if (value) return RGBA.fromHex(value)
|
||||||
|
return ansiToRgba(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const grays = generateGrayScale(bg, isDark)
|
||||||
|
const textMuted = generateMutedTextColor(bg, isDark)
|
||||||
|
const ansiColors = {
|
||||||
|
red: col(1),
|
||||||
|
green: col(2),
|
||||||
|
yellow: col(3),
|
||||||
|
blue: col(4),
|
||||||
|
magenta: col(5),
|
||||||
|
cyan: col(6),
|
||||||
|
redBright: col(9),
|
||||||
|
greenBright: col(10),
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffAlpha = isDark ? 0.22 : 0.14
|
||||||
|
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
||||||
|
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
||||||
|
const diffContextBg = grays[2]
|
||||||
|
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
||||||
|
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme: {
|
||||||
|
primary: ansiColors.cyan,
|
||||||
|
secondary: ansiColors.magenta,
|
||||||
|
accent: ansiColors.cyan,
|
||||||
|
error: ansiColors.red,
|
||||||
|
warning: ansiColors.yellow,
|
||||||
|
success: ansiColors.green,
|
||||||
|
info: ansiColors.cyan,
|
||||||
|
text: fg,
|
||||||
|
textMuted,
|
||||||
|
selectedListItemText: bg,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateGrayScale(bg: RGBA, isDark: boolean): Record<number, RGBA> {
|
||||||
|
const grays: Record<number, RGBA> = {}
|
||||||
|
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
|
||||||
|
|
||||||
|
if (isDark && luminance < 10) {
|
||||||
|
const gray = Math.floor(factor * 0.4 * 255)
|
||||||
|
grays[i] = RGBA.fromInts(gray, gray, gray)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDark && luminance > 245) {
|
||||||
|
const gray = Math.floor(255 - factor * 0.4 * 255)
|
||||||
|
grays[i] = RGBA.fromInts(gray, gray, gray)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = isDark ? luminance + (255 - luminance) * factor * 0.4 : luminance * (1 - factor * 0.4)
|
||||||
|
const ratio = next / luminance
|
||||||
|
grays[i] = RGBA.fromInts(
|
||||||
|
Math.floor(Math.min(Math.max(bgR * ratio, 0), 255)),
|
||||||
|
Math.floor(Math.min(Math.max(bgG * ratio, 0), 255)),
|
||||||
|
Math.floor(Math.min(Math.max(bgB * ratio, 0), 255)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grays
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA {
|
||||||
|
const luminance = 0.299 * bg.r * 255 + 0.587 * bg.g * 255 + 0.114 * bg.b * 255
|
||||||
|
if (isDark) {
|
||||||
|
const gray = luminance < 10 ? 180 : Math.min(Math.floor(160 + luminance * 0.3), 200)
|
||||||
|
return RGBA.fromInts(gray, gray, gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gray = luminance > 245 ? 75 : Math.max(Math.floor(100 - (255 - luminance) * 0.2), 60)
|
||||||
|
return RGBA.fromInts(gray, gray, gray)
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,9 @@ import { expect, test } from "bun:test"
|
||||||
import { mkdir, writeFile } from "node:fs/promises"
|
import { mkdir, writeFile } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import type { TerminalColors } from "@opentui/core"
|
import type { TerminalColors } from "@opentui/core"
|
||||||
import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme, terminalMode } from "../src/theme"
|
import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme } from "../src/theme"
|
||||||
import { discoverThemes } from "../src/context/theme"
|
import { discoverThemes } from "../src/context/theme"
|
||||||
|
import { terminalMode } from "../src/theme/system"
|
||||||
import { tmpdir } from "./fixture/fixture"
|
import { tmpdir } from "./fixture/fixture"
|
||||||
|
|
||||||
test("addTheme writes into module theme store", () => {
|
test("addTheme writes into module theme store", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue