11/**
2- * Strip ANSI escape codes from a string.
2+ * Strip ANSI escape codes and other terminal control characters from a string.
33 *
44 * Removes:
55 * - All CSI sequences (\x1b[...X) - SGR, cursor movement, erase, scroll, etc.
6- * - OSC 8 hyperlinks (\x1b]8;;URL \x07)
6+ * - OSC sequences (\x1b]... \x07 or \x1b]...\x1b\\ )
77 * - APC sequences (\x1b_...\x07 or \x1b_...\x1b\\)
8+ * - Remaining C0 control chars except tab/newline
89 */
910/**
1011 * Check if a string contains ANSI escape codes.
@@ -18,19 +19,33 @@ export function stripAnsi(str: string): string {
1819 const ESC = String . fromCodePoint ( 0x001b ) ;
1920 const BEL = String . fromCodePoint ( 0x0007 ) ;
2021
21- if ( ! str . includes ( ESC ) ) {
22- return str ;
23- }
22+ let clean = str ;
2423
25- // Strip all CSI sequences (ESC[...X where X is any letter)
26- let clean = str . replace ( new RegExp ( `${ ESC } \\[[0-9;]*[A-Za-z]` , "gu" ) , "" ) ;
27- // Strip OSC 8 hyperlinks: ESC]8;;URL<BEL> and ESC]8;;<BEL>
28- clean = clean . replace ( new RegExp ( `${ ESC } \\]8;;[^${ BEL } ]*${ BEL } ` , "gu" ) , "" ) ;
29- // Strip APC sequences: ESC_...<BEL> or ESC_...<ESC>\\ (used for cursor marker)
30- clean = clean . replace (
31- new RegExp ( `${ ESC } _[^${ BEL } ${ ESC } ]*(?:${ BEL } |${ ESC } \\\\)` , "gu" ) ,
32- "" ,
33- ) ;
24+ 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+ ) ;
37+ }
3438
35- return clean ;
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 ( "" ) ;
3651}
0 commit comments