|
7 | 7 | * - APC sequences (\x1b_...\x07 or \x1b_...\x1b\\) |
8 | 8 | * - Remaining C0 control chars except tab/newline |
9 | 9 | */ |
| 10 | +const ESC = String.fromCodePoint(0x001b); |
| 11 | +const BEL = String.fromCodePoint(0x0007); |
| 12 | + |
| 13 | +const ANSI_REPLACEMENTS: RegExp[] = [ |
| 14 | + // CSI sequences: SGR, cursor movement, erase, scroll, etc. |
| 15 | + new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "gu"), |
| 16 | + // OSC sequences: ESC]...<BEL> or ESC]...<ESC>\\. |
| 17 | + new RegExp(`${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"), |
| 18 | + // APC sequences: ESC_...<BEL> or ESC_...<ESC>\\. |
| 19 | + new RegExp(`${ESC}_[^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"), |
| 20 | +]; |
| 21 | + |
| 22 | +// Strip C0 terminal control characters that can corrupt TUI layout when |
| 23 | +// rendered back into pi, such as carriage return and backspace. Keep tabs and |
| 24 | +// newlines because logs use them as printable whitespace/line breaks. |
| 25 | +// biome-ignore lint/suspicious/noControlCharactersInRegex: this regex intentionally targets terminal control characters. |
| 26 | +const TERMINAL_CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f]/gu; |
| 27 | + |
10 | 28 | /** |
11 | 29 | * Check if a string contains ANSI escape codes. |
12 | 30 | */ |
13 | 31 | export function hasAnsi(str: string): boolean { |
14 | | - return str.includes(String.fromCodePoint(0x001b)); |
| 32 | + return str.includes(ESC); |
15 | 33 | } |
16 | 34 |
|
17 | 35 | export function stripAnsi(str: string): string { |
18 | | - // ESC = \u001b, BEL = \u0007 |
19 | | - const ESC = String.fromCodePoint(0x001b); |
20 | | - const BEL = String.fromCodePoint(0x0007); |
21 | | - |
22 | 36 | let clean = str; |
23 | 37 |
|
24 | 38 | if (str.includes(ESC)) { |
25 | | - // Strip all CSI sequences (ESC[...X where X is any letter) |
26 | | - clean = clean.replace(new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "gu"), ""); |
27 | | - // Strip OSC sequences: ESC]...<BEL> or ESC]...<ESC>\\ |
28 | | - clean = clean.replace( |
29 | | - new RegExp(`${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"), |
30 | | - "", |
31 | | - ); |
32 | | - // Strip APC sequences: ESC_...<BEL> or ESC_...<ESC>\\ (used for cursor marker) |
33 | | - clean = clean.replace( |
34 | | - new RegExp(`${ESC}_[^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)`, "gu"), |
35 | | - "", |
36 | | - ); |
| 39 | + for (const pattern of ANSI_REPLACEMENTS) { |
| 40 | + clean = clean.replace(pattern, ""); |
| 41 | + } |
37 | 42 | } |
38 | 43 |
|
39 | | - // Strip terminal control chars like carriage return/backspace that can |
40 | | - // corrupt TUI layout when rendered back into pi. |
41 | | - return Array.from(clean) |
42 | | - .filter((char) => { |
43 | | - const code = char.codePointAt(0) ?? 0; |
44 | | - const isDisallowedC0 = |
45 | | - (code >= 0x00 && code <= 0x08) || |
46 | | - (code >= 0x0b && code <= 0x1f) || |
47 | | - code === 0x7f; |
48 | | - return !isDisallowedC0; |
49 | | - }) |
50 | | - .join(""); |
| 44 | + return clean.replace(TERMINAL_CONTROL_CHARS, ""); |
51 | 45 | } |
0 commit comments