1pub struct AnsiUtils;
8
9impl AnsiUtils {
10 pub fn strip_ansi(text: &str) -> String {
24 let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap();
25 re.replace_all(text, "").to_string()
26 }
27
28 pub fn strip_control_chars(text: &str) -> String {
44 text.chars()
45 .filter(|c| {
46 !matches!(*c as u32,
48 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
49 )
50 })
51 .collect()
52 }
53
54 pub fn strip_all(text: &str) -> String {
58 Self::strip_control_chars(&Self::strip_ansi(text))
59 }
60
61 pub fn clean_for_processing(data: &str) -> String {
66 Self::strip_all(data)
67 }
68}
69
70#[derive(Debug, Clone)]
75pub struct AnsiConfig {
76 pub preserve_ansi: bool,
78 pub preserve_control_chars: bool,
80}
81
82impl Default for AnsiConfig {
83 fn default() -> Self {
84 AnsiConfig {
85 preserve_ansi: true,
86 preserve_control_chars: true,
87 }
88 }
89}
90
91impl AnsiConfig {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn strip_all() -> Self {
99 AnsiConfig {
100 preserve_ansi: false,
101 preserve_control_chars: false,
102 }
103 }
104
105 pub fn process_output(&self, data: &str) -> String {
109 if !self.preserve_ansi && !self.preserve_control_chars {
110 AnsiUtils::clean_for_processing(data)
111 } else if !self.preserve_ansi {
112 AnsiUtils::strip_ansi(data)
113 } else if !self.preserve_control_chars {
114 AnsiUtils::strip_control_chars(data)
115 } else {
116 data.to_string()
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_strip_ansi() {
127 let text = "\x1b[31mRed text\x1b[0m";
128 assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
129 }
130
131 #[test]
132 fn test_strip_ansi_multiple_codes() {
133 let text = "\x1b[1m\x1b[32mBold Green\x1b[0m Normal";
134 assert_eq!(AnsiUtils::strip_ansi(text), "Bold Green Normal");
135 }
136
137 #[test]
138 fn test_strip_control_chars() {
139 let text = "Hello\x00World\nNew line\tTab";
140 assert_eq!(
141 AnsiUtils::strip_control_chars(text),
142 "HelloWorld\nNew line\tTab"
143 );
144 }
145
146 #[test]
147 fn test_strip_control_chars_preserves_whitespace() {
148 let text = "Line1\nLine2\r\nLine3\tTabbed";
149 assert_eq!(
150 AnsiUtils::strip_control_chars(text),
151 "Line1\nLine2\r\nLine3\tTabbed"
152 );
153 }
154
155 #[test]
156 fn test_strip_all() {
157 let text = "\x1b[31mRed\x00text\x1b[0m";
158 assert_eq!(AnsiUtils::strip_all(text), "Redtext");
159 }
160
161 #[test]
162 fn test_ansi_config_default() {
163 let config = AnsiConfig::default();
164 let text = "\x1b[31mRed\x00text\x1b[0m";
165 assert_eq!(config.process_output(text), text);
166 }
167
168 #[test]
169 fn test_ansi_config_strip_all() {
170 let config = AnsiConfig::strip_all();
171 let text = "\x1b[31mRed\x00text\x1b[0m";
172 assert_eq!(config.process_output(text), "Redtext");
173 }
174
175 #[test]
176 fn test_ansi_config_strip_ansi_only() {
177 let config = AnsiConfig {
178 preserve_ansi: false,
179 preserve_control_chars: true,
180 };
181 let text = "\x1b[31mRed text\x1b[0m";
182 assert_eq!(config.process_output(text), "Red text");
183 }
184
185 #[test]
186 fn test_ansi_config_strip_control_only() {
187 let config = AnsiConfig {
188 preserve_ansi: true,
189 preserve_control_chars: false,
190 };
191 let text = "Hello\x00World";
192 assert_eq!(config.process_output(text), "HelloWorld");
193 }
194}