mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
fix(tui): normalize legacy Terminal.app colors (#17695)
Keep light Terminal.app TUI colors readable by normalizing non-banner theme tokens into ANSI256-safe buckets while preserving truecolor terminals.
This commit is contained in:
parent
31f70d1f2a
commit
4cc6da84a1
9 changed files with 487 additions and 50 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR'] as const
|
||||
const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR', 'TERM', 'TERM_PROGRAM'] as const
|
||||
let importId = 0
|
||||
|
||||
async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
|
||||
const saved: Record<string, string | undefined> = {}
|
||||
|
|
@ -25,11 +26,39 @@ async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
|
|||
}
|
||||
|
||||
describe('forceTruecolor', () => {
|
||||
it('sets COLORTERM=truecolor and FORCE_COLOR=3 when unset', async () => {
|
||||
it('does not force truecolor by default', async () => {
|
||||
await withCleanEnv(
|
||||
() => {},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=default-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does not infer truecolor from Apple Terminal on pre-Tahoe macOS', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.TERM_PROGRAM = 'Apple_Terminal'
|
||||
process.env.TERM = 'xterm-256color'
|
||||
},
|
||||
async () => {
|
||||
const mod = await import('../lib/forceTruecolor.js?t=apple-' + importId++)
|
||||
expect(mod.shouldForceTruecolor({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('sets COLORTERM=truecolor and FORCE_COLOR=3 when explicitly enabled', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=enabled-' + importId++)
|
||||
expect(process.env.COLORTERM).toBe('truecolor')
|
||||
expect(process.env.FORCE_COLOR).toBe('3')
|
||||
}
|
||||
|
|
@ -40,9 +69,10 @@ describe('forceTruecolor', () => {
|
|||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.HERMES_TUI_TRUECOLOR = '0'
|
||||
process.env.TERM_PROGRAM = 'Apple_Terminal'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=optout-' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=optout-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
|
|
@ -53,12 +83,41 @@ describe('forceTruecolor', () => {
|
|||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.NO_COLOR = '1'
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=no-color-' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=no-color-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('respects existing FORCE_COLOR unless Hermes truecolor is explicit', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.FORCE_COLOR = ''
|
||||
},
|
||||
async () => {
|
||||
const mod = await import('../lib/forceTruecolor.js?t=force-color-' + importId++)
|
||||
expect(mod.shouldForceTruecolor(process.env)).toBe(false)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBe('')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('lets explicit Hermes truecolor override existing FORCE_COLOR', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.FORCE_COLOR = '0'
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=explicit-force-' + importId++)
|
||||
expect(process.env.COLORTERM).toBe('truecolor')
|
||||
expect(process.env.FORCE_COLOR).toBe('3')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ const RELEVANT_ENV = [
|
|||
'HERMES_TUI_THEME',
|
||||
'HERMES_TUI_BACKGROUND',
|
||||
'COLORFGBG',
|
||||
'COLORTERM',
|
||||
'TERM_PROGRAM'
|
||||
] as const
|
||||
|
||||
async function importThemeWithCleanEnv() {
|
||||
async function importThemeWithEnv(env: Partial<Record<(typeof RELEVANT_ENV)[number], string>> = {}) {
|
||||
for (const key of RELEVANT_ENV) {
|
||||
vi.stubEnv(key, '')
|
||||
vi.stubEnv(key, env[key] ?? '')
|
||||
}
|
||||
|
||||
vi.resetModules()
|
||||
|
|
@ -29,6 +30,10 @@ async function importThemeWithCleanEnv() {
|
|||
return import('../theme.js')
|
||||
}
|
||||
|
||||
async function importThemeWithCleanEnv() {
|
||||
return importThemeWithEnv()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.resetModules()
|
||||
|
|
@ -84,6 +89,12 @@ describe('detectLightMode', () => {
|
|||
expect(detectLightMode({})).toBe(false)
|
||||
})
|
||||
|
||||
it('defaults Apple Terminal to light when no stronger signal is present', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
|
||||
expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(true)
|
||||
})
|
||||
|
||||
it('honors HERMES_TUI_LIGHT on/off', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
|
||||
|
|
@ -159,8 +170,8 @@ describe('detectLightMode', () => {
|
|||
|
||||
it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
// Inject a light-default allow-list so the precedence test is
|
||||
// meaningful even though the production allow-list is empty.
|
||||
// Injecting the allow-list keeps this precedence rule explicit even if
|
||||
// production defaults change.
|
||||
const allowList = new Set(['Apple_Terminal'])
|
||||
|
||||
// Sanity: the allow-list alone WOULD turn this terminal light.
|
||||
|
|
@ -221,6 +232,40 @@ describe('fromSkin', () => {
|
|||
expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)
|
||||
})
|
||||
|
||||
it('normalizes non-banner foregrounds on light Apple Terminal', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: 'Apple_Terminal' })
|
||||
|
||||
const theme = fromSkin({
|
||||
banner_accent: '#FFBF00',
|
||||
banner_border: '#CD7F32',
|
||||
banner_dim: '#B8860B',
|
||||
banner_text: '#FFF8DC',
|
||||
banner_title: '#FFD700',
|
||||
prompt: '#FFF8DC'
|
||||
}, {})
|
||||
|
||||
expect(theme.color.primary).toBe('#FFD700')
|
||||
expect(theme.color.accent).toBe('#FFBF00')
|
||||
expect(theme.color.border).toBe('#CD7F32')
|
||||
expect(theme.color.muted).toBe('ansi256(245)')
|
||||
expect(theme.color.text).toBe('ansi256(136)')
|
||||
expect(theme.color.prompt).toBe('ansi256(136)')
|
||||
})
|
||||
|
||||
it('does not normalize light Apple Terminal when truecolor is advertised', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' })
|
||||
const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
|
||||
|
||||
expect(theme.color.text).toBe('#FFF8DC')
|
||||
})
|
||||
|
||||
it('normalizes Apple Terminal names before matching', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: ' Apple_Terminal ' })
|
||||
const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
|
||||
|
||||
expect(theme.color.text).toBe('ansi256(136)')
|
||||
})
|
||||
|
||||
it('passes banner logo/hero', async () => {
|
||||
const { fromSkin } = await importThemeWithCleanEnv()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
#!/usr/bin/env -S node --max-old-space-size=8192 --expose-gc
|
||||
// Must be first import — mutates process.env.FORCE_COLOR / COLORTERM before
|
||||
// any chalk / supports-color import so the banner gradient renders in
|
||||
// truecolor instead of being downsampled to 256-color (which collapses
|
||||
// gold #FFD700 and amber #FFBF00 to the same slot).
|
||||
// Must be first import. If the user explicitly opts into truecolor, this
|
||||
// nudges chalk / supports-color before either package is initialized.
|
||||
import './lib/forceTruecolor.js'
|
||||
|
||||
import type { FrameEvent } from '@hermes/ink'
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
/**
|
||||
* Force 24-bit truecolor output before any chalk / supports-color import.
|
||||
* Targeted 24-bit truecolor override before chalk / supports-color imports.
|
||||
*
|
||||
* Why this exists:
|
||||
* The base CLI (Python/Rich) emits banner colors as truecolor ANSI
|
||||
* (`\033[38;2;R;G;Bm`). The TUI renders through Ink → chalk, whose
|
||||
* supports-color auto-detection defaults to 256-color on macOS Terminal.app
|
||||
* and any terminal that does NOT set `COLORTERM=truecolor`. In 256-color
|
||||
* mode, chalk downsamples `#FFD700` (gold) and `#FFBF00` (amber) to the
|
||||
* *same* xterm-256 palette slot (220) — collapsing the banner gradient
|
||||
* into a single flat yellow band. The bronze and dim rows also lose
|
||||
* contrast against each other.
|
||||
*
|
||||
* Terminal.app (macOS 12+), iTerm2, kitty, Alacritty, VS Code, Cursor,
|
||||
* and WezTerm all render truecolor correctly. The few that don't
|
||||
* (ancient xterm, some CI environments) can set `HERMES_TUI_TRUECOLOR=0`
|
||||
* to opt out.
|
||||
*
|
||||
* This MUST run before any `chalk` or `supports-color` import. supports-color
|
||||
* caches its level on first load, so nudging env vars after that point has
|
||||
* no effect.
|
||||
* macOS Terminal.app before Tahoe 26 does not support RGB SGR, so do not
|
||||
* infer truecolor from TERM_PROGRAM=Apple_Terminal. Users can still opt in
|
||||
* explicitly on terminals that support RGB but do not advertise COLORTERM.
|
||||
*/
|
||||
|
||||
if (process.env.HERMES_TUI_TRUECOLOR !== '0' && !process.env.NO_COLOR && !process.env.FORCE_COLOR) {
|
||||
const TRUE_RE = /^(?:1|true|yes|on)$/i
|
||||
const FALSE_RE = /^(?:0|false|no|off)$/i
|
||||
|
||||
export function shouldForceTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const override = (env.HERMES_TUI_TRUECOLOR ?? '').trim()
|
||||
|
||||
if (FALSE_RE.test(override) || 'NO_COLOR' in env) {
|
||||
return false
|
||||
}
|
||||
|
||||
return TRUE_RE.test(override)
|
||||
}
|
||||
|
||||
if (shouldForceTruecolor()) {
|
||||
if (!process.env.COLORTERM) {
|
||||
process.env.COLORTERM = 'truecolor'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,162 @@ function mix(a: string, b: string, t: number) {
|
|||
return '#' + ((1 << 24) | (lerp(0) << 16) | (lerp(1) << 8) | lerp(2)).toString(16).slice(1)
|
||||
}
|
||||
|
||||
const XTERM_6_LEVELS = [0, 95, 135, 175, 215, 255] as const
|
||||
const ANSI_LIGHT_MAX_LUMINANCE = 0.72
|
||||
const ANSI_LIGHT_TARGET_LUMINANCE = 0.34
|
||||
const ANSI_LIGHT_MIN_SATURATION = 0.22
|
||||
const ANSI_MUTED_BUCKET = 245
|
||||
|
||||
const ANSI_NORMALIZED_FOREGROUNDS: readonly (keyof ThemeColors)[] = [
|
||||
'text',
|
||||
'label',
|
||||
'ok',
|
||||
'error',
|
||||
'warn',
|
||||
'prompt',
|
||||
'statusFg',
|
||||
'statusGood',
|
||||
'statusWarn',
|
||||
'statusBad',
|
||||
'statusCritical',
|
||||
'shellDollar'
|
||||
]
|
||||
|
||||
const ANSI_MUTED_FOREGROUNDS: readonly (keyof ThemeColors)[] = ['muted', 'sessionLabel', 'sessionBorder']
|
||||
|
||||
function xtermEightBitRgb(colorNumber: number): [number, number, number] {
|
||||
if (colorNumber >= 232) {
|
||||
const value = 8 + (colorNumber - 232) * 10
|
||||
|
||||
return [value, value, value]
|
||||
}
|
||||
|
||||
if (colorNumber >= 16) {
|
||||
const offset = colorNumber - 16
|
||||
|
||||
return [
|
||||
XTERM_6_LEVELS[Math.floor(offset / 36) % 6]!,
|
||||
XTERM_6_LEVELS[Math.floor(offset / 6) % 6]!,
|
||||
XTERM_6_LEVELS[offset % 6]!
|
||||
]
|
||||
}
|
||||
|
||||
return [0, 0, 0]
|
||||
}
|
||||
|
||||
function channelLuminance(value: number): number {
|
||||
const normalized = value / 255
|
||||
|
||||
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4
|
||||
}
|
||||
|
||||
function relativeLuminance(red: number, green: number, blue: number): number {
|
||||
return 0.2126 * channelLuminance(red) + 0.7152 * channelLuminance(green) + 0.0722 * channelLuminance(blue)
|
||||
}
|
||||
|
||||
function rgbToHsl(red: number, green: number, blue: number): [number, number, number] {
|
||||
const rn = red / 255
|
||||
const gn = green / 255
|
||||
const bn = blue / 255
|
||||
const max = Math.max(rn, gn, bn)
|
||||
const min = Math.min(rn, gn, bn)
|
||||
const lightness = (max + min) / 2
|
||||
|
||||
if (max === min) {
|
||||
return [0, 0, lightness]
|
||||
}
|
||||
|
||||
const delta = max - min
|
||||
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min)
|
||||
|
||||
const hue =
|
||||
max === rn
|
||||
? (gn - bn) / delta + (gn < bn ? 6 : 0)
|
||||
: max === gn
|
||||
? (bn - rn) / delta + 2
|
||||
: (rn - gn) / delta + 4
|
||||
|
||||
return [hue / 6, saturation, lightness]
|
||||
}
|
||||
|
||||
function circularDistance(a: number, b: number): number {
|
||||
const distance = Math.abs(a - b)
|
||||
|
||||
return Math.min(distance, 1 - distance)
|
||||
}
|
||||
|
||||
// Mirrors @hermes/ink's colorize.ts. Keep local: app code compiles from
|
||||
// ui-tui/src, while @hermes/ink is bundled separately from packages/.
|
||||
function richEightBitColorNumber(red: number, green: number, blue: number): number {
|
||||
const [, saturation, lightness] = rgbToHsl(red, green, blue)
|
||||
|
||||
if (saturation < 0.15) {
|
||||
const gray = Math.round(lightness * 25)
|
||||
|
||||
return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray
|
||||
}
|
||||
|
||||
const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40
|
||||
const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40
|
||||
const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40
|
||||
|
||||
return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue)
|
||||
}
|
||||
|
||||
function bestReadableAnsiColor(red: number, green: number, blue: number): number {
|
||||
const [hue, saturation, lightness] = rgbToHsl(red, green, blue)
|
||||
let bestColor = richEightBitColorNumber(red, green, blue)
|
||||
let bestScore = Number.POSITIVE_INFINITY
|
||||
|
||||
for (let colorNumber = 16; colorNumber <= 255; colorNumber += 1) {
|
||||
const [candidateRed, candidateGreen, candidateBlue] = xtermEightBitRgb(colorNumber)
|
||||
const candidateLuminance = relativeLuminance(candidateRed, candidateGreen, candidateBlue)
|
||||
|
||||
if (candidateLuminance > ANSI_LIGHT_MAX_LUMINANCE) {
|
||||
continue
|
||||
}
|
||||
|
||||
const [candidateHue, candidateSaturation, candidateLightness] = rgbToHsl(
|
||||
candidateRed,
|
||||
candidateGreen,
|
||||
candidateBlue
|
||||
)
|
||||
|
||||
const saturationFloorPenalty =
|
||||
candidateSaturation < ANSI_LIGHT_MIN_SATURATION ? (ANSI_LIGHT_MIN_SATURATION - candidateSaturation) * 3 : 0
|
||||
|
||||
const score =
|
||||
circularDistance(candidateHue, hue) * 4 +
|
||||
Math.abs(candidateSaturation - Math.max(ANSI_LIGHT_MIN_SATURATION, saturation)) * 0.8 +
|
||||
Math.abs(candidateLightness - Math.min(lightness, ANSI_LIGHT_TARGET_LUMINANCE)) * 2 +
|
||||
saturationFloorPenalty
|
||||
|
||||
if (score < bestScore) {
|
||||
bestColor = colorNumber
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
|
||||
return bestColor
|
||||
}
|
||||
|
||||
function normalizeAnsiForeground(color: string): string {
|
||||
const rgb = parseHex(color)
|
||||
|
||||
if (!rgb) {
|
||||
return color
|
||||
}
|
||||
|
||||
const richAnsi = richEightBitColorNumber(rgb[0], rgb[1], rgb[2])
|
||||
const richRgb = xtermEightBitRgb(richAnsi)
|
||||
|
||||
const ansi = relativeLuminance(richRgb[0], richRgb[1], richRgb[2]) > ANSI_LIGHT_MAX_LUMINANCE
|
||||
? bestReadableAnsiColor(rgb[0], rgb[1], rgb[2])
|
||||
: richAnsi
|
||||
|
||||
return `ansi256(${ansi})`
|
||||
}
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────
|
||||
|
||||
const BRAND: ThemeBrand = {
|
||||
|
|
@ -190,12 +346,11 @@ export const LIGHT_THEME: Theme = {
|
|||
const TRUE_RE = /^(?:1|true|yes|on)$/
|
||||
const FALSE_RE = /^(?:0|false|no|off)$/
|
||||
|
||||
// Reserved for future TERM_PROGRAM-based heuristics. Empty by default:
|
||||
// most modern terminals (Ghostty, Warp, iTerm2, Apple_Terminal) ship a
|
||||
// dark profile out of the box, so guessing wrong here is more annoying
|
||||
// than missing a light user — light users can always set
|
||||
// `HERMES_TUI_LIGHT=1` or `HERMES_TUI_THEME=light`.
|
||||
const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>()
|
||||
// TERM_PROGRAM fallback allow-list for terminals whose default profile is
|
||||
// light and which may not expose COLORFGBG. This currently includes Apple
|
||||
// Terminal. Explicit HERMES_TUI_THEME / COLORFGBG signals above still win,
|
||||
// so dark Apple Terminal profiles that advertise a dark background stay dark.
|
||||
const LIGHT_DEFAULT_TERM_PROGRAMS = new Set<string>(['Apple_Terminal'])
|
||||
|
||||
// Best-effort RGB → luminance check. Currently only accepts a 3- or
|
||||
// 6-digit hex value (with or without a leading `#`); the env var name
|
||||
|
|
@ -247,7 +402,7 @@ function backgroundLuminance(raw: string): null | number {
|
|||
// slot 7 or 15 on light profiles; 0–15 ranges are otherwise
|
||||
// treated as authoritatively dark so the TERM_PROGRAM
|
||||
// allow-list below cannot override an explicit dark profile.
|
||||
// 5. `TERM_PROGRAM` light-default allow-list (currently empty).
|
||||
// 5. `TERM_PROGRAM` light-default allow-list.
|
||||
//
|
||||
// Anything we can't decide stays dark — the default Hermes palette
|
||||
// is the dark one.
|
||||
|
|
@ -313,7 +468,42 @@ export function detectLightMode(
|
|||
return lightDefaultTermPrograms.has(termProgram)
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME: Theme = detectLightMode() ? LIGHT_THEME : DARK_THEME
|
||||
function shouldNormalizeAnsiLightTheme(env: NodeJS.ProcessEnv = process.env, isLight = detectLightMode(env)): boolean {
|
||||
const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase()
|
||||
const termProgram = (env.TERM_PROGRAM ?? '').trim()
|
||||
|
||||
return termProgram === 'Apple_Terminal' && colorTerm !== 'truecolor' && colorTerm !== '24bit' && isLight
|
||||
}
|
||||
|
||||
export function normalizeThemeForAnsiLightTerminal(
|
||||
theme: Theme,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
isLight = detectLightMode(env)
|
||||
): Theme {
|
||||
if (!shouldNormalizeAnsiLightTheme(env, isLight)) {
|
||||
return theme
|
||||
}
|
||||
|
||||
const color = { ...theme.color }
|
||||
|
||||
for (const key of ANSI_NORMALIZED_FOREGROUNDS) {
|
||||
color[key] = normalizeAnsiForeground(color[key])
|
||||
}
|
||||
|
||||
for (const key of ANSI_MUTED_FOREGROUNDS) {
|
||||
color[key] = `ansi256(${ANSI_MUTED_BUCKET})`
|
||||
}
|
||||
|
||||
return { ...theme, color }
|
||||
}
|
||||
|
||||
const DEFAULT_LIGHT_MODE = detectLightMode()
|
||||
|
||||
export const DEFAULT_THEME: Theme = normalizeThemeForAnsiLightTerminal(
|
||||
DEFAULT_LIGHT_MODE ? LIGHT_THEME : DARK_THEME,
|
||||
process.env,
|
||||
DEFAULT_LIGHT_MODE
|
||||
)
|
||||
|
||||
// ── Skin → Theme ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -333,7 +523,7 @@ export function fromSkin(
|
|||
const muted = c('banner_dim') ?? d.color.muted
|
||||
const completionBg = c('completion_menu_bg') ?? d.color.completionBg
|
||||
|
||||
return {
|
||||
return normalizeThemeForAnsiLightTerminal({
|
||||
color: {
|
||||
primary: c('ui_primary') ?? c('banner_title') ?? d.color.primary,
|
||||
accent,
|
||||
|
|
@ -379,5 +569,5 @@ export function fromSkin(
|
|||
|
||||
bannerLogo,
|
||||
bannerHero
|
||||
}
|
||||
}, process.env, DEFAULT_LIGHT_MODE)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue