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:
brooklyn! 2026-04-29 20:13:49 -07:00 committed by GitHub
parent 31f70d1f2a
commit 4cc6da84a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 487 additions and 50 deletions

View file

@ -1,18 +1,38 @@
import { describe, expect, it } from 'vitest'
import { shouldUseAnsiDim } from './Text.js'
import { dimColorFallback, shouldUseAnsiDim } from './Text.js'
describe('shouldUseAnsiDim', () => {
it('disables ANSI dim on VTE terminals by default', () => {
expect(shouldUseAnsiDim({ VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(false)
})
it('disables ANSI dim on Apple Terminal by default', () => {
expect(shouldUseAnsiDim({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
})
it('keeps ANSI dim enabled elsewhere by default', () => {
expect(shouldUseAnsiDim({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)
})
it('honors explicit env override', () => {
expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', VTE_VERSION: '7603' } as NodeJS.ProcessEnv)).toBe(true)
expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(true)
expect(shouldUseAnsiDim({ HERMES_TUI_DIM: '0' } as NodeJS.ProcessEnv)).toBe(false)
})
})
describe('dimColorFallback', () => {
it('renders Apple Terminal dim as muted gray by default', () => {
expect(dimColorFallback({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe('#6B7280')
})
it('normalizes Apple Terminal names before matching', () => {
expect(dimColorFallback({ TERM_PROGRAM: ' Apple_Terminal ' } as NodeJS.ProcessEnv)).toBe('#6B7280')
})
it('does not apply when dim is explicitly configured', () => {
expect(dimColorFallback({ HERMES_TUI_DIM: '1', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()
expect(dimColorFallback({ HERMES_TUI_DIM: '0', TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBeUndefined()
})
})

View file

@ -6,6 +6,7 @@ import type { Color, Styles } from '../styles.js'
const ENV_ON_RE = /^(?:1|true|yes|on)$/i
const ENV_OFF_RE = /^(?:0|false|no|off)$/i
const LEGACY_APPLE_DIM_COLOR: Color = '#6B7280'
type BaseProps = {
/**
* Change text color. Accepts a raw color value (rgb, hex, ansi).
@ -76,9 +77,23 @@ export function shouldUseAnsiDim(env: NodeJS.ProcessEnv = process.env): boolean
return false
}
if ((env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal') {
return false
}
return !env.VTE_VERSION
}
export function dimColorFallback(env: NodeJS.ProcessEnv = process.env): Color | undefined {
const override = (env.HERMES_TUI_DIM ?? '').trim()
if (ENV_ON_RE.test(override) || ENV_OFF_RE.test(override)) {
return undefined
}
return (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal' ? LEGACY_APPLE_DIM_COLOR : undefined
}
const memoizedStylesForWrap: Record<NonNullable<Styles['textWrap']>, Styles> = {
wrap: {
flexGrow: 0,
@ -161,6 +176,7 @@ export default function Text(t0: Props) {
const inverse = t4 === undefined ? false : t4
const wrap = t5 === undefined ? 'wrap' : t5
const effectiveDim = dim && shouldUseAnsiDim()
const effectiveColor = dim && !effectiveDim ? (color ?? dimColorFallback()) : color
if (children === undefined || children === null) {
return null
@ -168,11 +184,11 @@ export default function Text(t0: Props) {
let t6
if ($[0] !== color) {
t6 = color && {
color
if ($[0] !== effectiveColor) {
t6 = effectiveColor && {
color: effectiveColor
}
$[0] = color
$[0] = effectiveColor
$[1] = t6
} else {
t6 = $[1]