const ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g; export const ANSI_ESCAPE_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g; export const ANSI_GREEN_FG = "\x1b[32m"; export const ANSI_RED_FG = "\x1b[31m"; export const ANSI_YELLOW_FG = "\x1b[33m"; export const ANSI_FG_RESET = "\x1b[39m"; export function stripAnsi(text: string): string { return text.replace(ANSI_PATTERN, ""); } export function ansiGreen(text: string): string { return `${ANSI_GREEN_FG}${text}${ANSI_FG_RESET}`; } export function ansiRed(text: string): string { return `${ANSI_RED_FG}${text}${ANSI_FG_RESET}`; } export function ansiYellow(text: string): string { return `${ANSI_YELLOW_FG}${text}${ANSI_FG_RESET}`; } export function oneLine(text: string): string { return text .replace(ANSI_ESCAPE_RE, "") .replace(/[\r\n\t]+/g, " ") .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, " ") .replace(/\s+/g, " ") .trim(); } export interface VisibleMap { text: string; rawIndexByVisibleIndex: number[]; } export interface VisibleReplacement { start: number; end: number; text: string; } export function ansiSequenceEnd(input: string, start: number): number { const introducer = input[start + 1]; if (introducer == null) return start + 1; // OSC, DCS, APC, PM, SOS strings are zero-width and end in ST (ESC \\). // OSC may also end in BEL. if (introducer === "]" || introducer === "P" || introducer === "_" || introducer === "^" || introducer === "X") { const st = input.indexOf("\x1b\\", start + 2); const bell = introducer === "]" ? input.indexOf("\x07", start + 2) : -1; if (bell >= 0 && (st < 0 || bell < st)) return bell + 1; return st >= 0 ? st + 2 : input.length; } if (introducer === "[") { let index = start + 2; while (index < input.length) { const code = input.charCodeAt(index); if (code >= 0x40 && code <= 0x7e) return index + 1; index += 1; } return input.length; } return Math.min(start + 2, input.length); } export function buildVisibleMap(input: string): VisibleMap { const rawIndexByVisibleIndex: number[] = [0]; let text = ""; let index = 0; while (index < input.length) { if (input.charCodeAt(index) === 0x1b) { index = ansiSequenceEnd(input, index); continue; } if (rawIndexByVisibleIndex[text.length] === undefined) rawIndexByVisibleIndex[text.length] = index; text += input[index] ?? ""; index += 1; rawIndexByVisibleIndex[text.length] = index; } if (rawIndexByVisibleIndex[text.length] === undefined) rawIndexByVisibleIndex[text.length] = index; return { rawIndexByVisibleIndex, text }; } export function applyVisibleReplacements(input: string, map: VisibleMap, replacements: VisibleReplacement[]): string { if (replacements.length === 0) return input; const sorted = replacements .filter((replacement) => replacement.end > replacement.start) .sort((a, b) => a.start - b.start || b.end - a.end); let output = ""; let lastRawIndex = 0; let lastVisibleIndex = 0; for (const replacement of sorted) { if (replacement.start < lastVisibleIndex) continue; const rawStart = map.rawIndexByVisibleIndex[replacement.start]; const rawEnd = map.rawIndexByVisibleIndex[replacement.end]; if (rawStart == null || rawEnd == null || rawStart < lastRawIndex) continue; output += input.slice(lastRawIndex, rawStart) + replacement.text; lastRawIndex = rawEnd; lastVisibleIndex = replacement.end; } return output + input.slice(lastRawIndex); }