fix(tui): harden Terminal.app render behavior
Avoid Terminal.app paint corruption by disabling fast-echo in that terminal, sanitizing non-SGR control sequences before ANSI rendering, and defaulting Apple Terminal back to the safer 256-color path unless truecolor is explicitly requested.
This commit is contained in:
@@ -19,12 +19,42 @@ export function shouldForceTruecolor(env: NodeJS.ProcessEnv = process.env): bool
|
||||
return TRUE_RE.test(override)
|
||||
}
|
||||
|
||||
const isAppleTerminal = (env: NodeJS.ProcessEnv = process.env) => (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal'
|
||||
|
||||
const isAdvertisedTruecolor = (env: NodeJS.ProcessEnv = process.env) => {
|
||||
const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase()
|
||||
const forceColor = (env.FORCE_COLOR ?? '').trim()
|
||||
|
||||
return colorTerm === 'truecolor' || colorTerm === '24bit' || forceColor === '3'
|
||||
}
|
||||
|
||||
export function shouldDowngradeAppleTerminalTruecolor(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (!isAppleTerminal(env)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (shouldForceTruecolor(env)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isAdvertisedTruecolor(env)
|
||||
}
|
||||
|
||||
if (shouldForceTruecolor()) {
|
||||
if (!process.env.COLORTERM) {
|
||||
process.env.COLORTERM = 'truecolor'
|
||||
}
|
||||
|
||||
process.env.FORCE_COLOR = '3'
|
||||
} else if (shouldDowngradeAppleTerminalTruecolor()) {
|
||||
// Terminal.app may advertise truecolor even when RGB SGR paths render
|
||||
// incorrectly. Keep Hermes on the safer TERM-driven 256-color path unless
|
||||
// users explicitly opt back in via HERMES_TUI_TRUECOLOR=1.
|
||||
delete process.env.COLORTERM
|
||||
|
||||
if ((process.env.FORCE_COLOR ?? '').trim() === '3') {
|
||||
delete process.env.FORCE_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
@@ -9,12 +9,27 @@ import { VERBS } from '../content/verbs.js'
|
||||
import type { ThinkingMode } from '../types.js'
|
||||
|
||||
const ESC = String.fromCharCode(27)
|
||||
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;]*m`, 'g')
|
||||
const BEL = String.fromCharCode(7)
|
||||
const ANSI_CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'g')
|
||||
const ANSI_CSI_WITH_CMD_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*([@-~])`, 'g')
|
||||
const ANSI_OSC_RE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
|
||||
const ANSI_STRING_RE = new RegExp(`${ESC}[PX^_][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
|
||||
const ANSI_STRAY_ESC_RE = new RegExp(`${ESC}(?!\\[)[\\s\\S]?`, 'g')
|
||||
const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]/g
|
||||
const WS_RE = /\s+/g
|
||||
|
||||
export const stripAnsi = (s: string) => s.replace(ANSI_RE, '')
|
||||
export const stripAnsi = (s: string) =>
|
||||
s.replace(ANSI_OSC_RE, '').replace(ANSI_STRING_RE, '').replace(ANSI_CSI_RE, '').replace(ANSI_STRAY_ESC_RE, '').replace(CONTROL_RE, '')
|
||||
|
||||
export const hasAnsi = (s: string) => s.includes(`${ESC}[`) || s.includes(`${ESC}]`)
|
||||
export const sanitizeAnsiForRender = (s: string) =>
|
||||
s
|
||||
.replace(ANSI_OSC_RE, '')
|
||||
.replace(ANSI_STRING_RE, '')
|
||||
.replace(ANSI_CSI_WITH_CMD_RE, (seq, cmd: string) => (cmd === 'm' ? seq : ''))
|
||||
.replace(ANSI_STRAY_ESC_RE, '')
|
||||
.replace(CONTROL_RE, '')
|
||||
|
||||
export const hasAnsi = (s: string) => s.includes(ESC)
|
||||
|
||||
const renderEstimateLine = (line: string) => {
|
||||
const trimmed = line.trim()
|
||||
|
||||
Reference in New Issue
Block a user