1
2pub fn strip_ansi(input: &str) -> String {
3 let bytes = input.as_bytes();
4 let mut out = String::with_capacity(bytes.len());
5 let mut i = 0;
6 while i < bytes.len() {
7 let b = bytes[i];
8 if b == 0x1b && i + 1 < bytes.len() {
9 let next = bytes[i + 1];
10
11 if next == b'[' {
12 i += 2;
13 while i < bytes.len() {
14 let c = bytes[i];
15 if (0x40..=0x7e).contains(&c) {
16 i += 1;
17 break;
18 }
19 i += 1;
20 }
21 continue;
22 }
23
24 if next == b']' {
25 i += 2;
26 while i < bytes.len() {
27 let c = bytes[i];
28 if c == 0x07 {
29 i += 1;
30 break;
31 }
32 if c == 0x1b && i + 1 < bytes.len() && bytes[i + 1] == b'\\' {
33 i += 2;
34 break;
35 }
36 i += 1;
37 }
38 continue;
39 }
40
41 if matches!(next, b'(' | b')' | b'*' | b'+' | b'#') && i + 2 < bytes.len() {
42 i += 3;
43 continue;
44 }
45
46 i += 1;
47 continue;
48 }
49
50 if b < 0x20 && b != b'\n' && b != b'\t' && b != b'\r' {
51 i += 1;
52 continue;
53 }
54
55 let char_end = utf8_char_end(bytes, i);
56 out.push_str(&input[i..char_end]);
57 i = char_end;
58 }
59 out
60}
61
62fn utf8_char_end(bytes: &[u8], i: usize) -> usize {
63 let b = bytes[i];
64 let width = if b < 0x80 {
65 1
66 } else if b & 0xe0 == 0xc0 {
67 2
68 } else if b & 0xf0 == 0xe0 {
69 3
70 } else if b & 0xf8 == 0xf0 {
71 4
72 } else {
73 1
74 };
75 (i + width).min(bytes.len())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn strips_csi() {
84 assert_eq!(strip_ansi("\x1b[31mred\x1b[0m"), "red");
85 assert_eq!(strip_ansi("\x1b[1;33mwarn\x1b[m hi"), "warn hi");
86 }
87
88 #[test]
89 fn strips_osc() {
90 assert_eq!(strip_ansi("\x1b]0;title\x07after"), "after");
91 assert_eq!(strip_ansi("\x1b]0;title\x1b\\after"), "after");
92 }
93
94 #[test]
95 fn keeps_unicode() {
96 assert_eq!(strip_ansi("héllo 🦀"), "héllo 🦀");
97 }
98}