1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! ANSI control character utilities for command-stream
//!
//! This module handles stripping and processing of ANSI escape codes
//! and control characters from text output.
/// ANSI control character utilities
pub struct AnsiUtils;
impl AnsiUtils {
/// Strip ANSI escape sequences from text
///
/// Removes color codes, cursor movement, and other ANSI escape sequences
/// while preserving the actual text content.
///
/// # Examples
///
/// ```
/// use command_stream::ansi::AnsiUtils;
///
/// let text = "\x1b[31mRed text\x1b[0m";
/// assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
/// ```
pub fn strip_ansi(text: &str) -> String {
let re = regex::Regex::new(r"\x1b\[[0-9;]*[mGKHFJ]").unwrap();
re.replace_all(text, "").to_string()
}
/// Strip control characters from text, preserving newlines, carriage returns, and tabs
///
/// Removes control characters (ASCII 0x00-0x1F and 0x7F) except:
/// - Newlines (\n = 0x0A)
/// - Carriage returns (\r = 0x0D)
/// - Tabs (\t = 0x09)
///
/// # Examples
///
/// ```
/// use command_stream::ansi::AnsiUtils;
///
/// let text = "Hello\x00World\nNew line\tTab";
/// assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld\nNew line\tTab");
/// ```
pub fn strip_control_chars(text: &str) -> String {
text.chars()
.filter(|c| {
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
!matches!(*c as u32,
0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
)
})
.collect()
}
/// Strip both ANSI sequences and control characters
///
/// Combines `strip_ansi` and `strip_control_chars` for complete text cleaning.
pub fn strip_all(text: &str) -> String {
Self::strip_control_chars(&Self::strip_ansi(text))
}
/// Clean data for processing (strips ANSI and control chars)
///
/// Alias for `strip_all` - provides semantic clarity when processing
/// data that needs to be cleaned for further processing.
pub fn clean_for_processing(data: &str) -> String {
Self::strip_all(data)
}
}
/// Configuration for ANSI handling
///
/// Controls how ANSI escape codes and control characters are processed
/// in command output.
#[derive(Debug, Clone)]
pub struct AnsiConfig {
/// Whether to preserve ANSI escape sequences in output
pub preserve_ansi: bool,
/// Whether to preserve control characters in output
pub preserve_control_chars: bool,
}
impl Default for AnsiConfig {
fn default() -> Self {
AnsiConfig {
preserve_ansi: true,
preserve_control_chars: true,
}
}
}
impl AnsiConfig {
/// Create a new AnsiConfig that preserves everything (default)
pub fn new() -> Self {
Self::default()
}
/// Create a config that strips all ANSI and control characters
pub fn strip_all() -> Self {
AnsiConfig {
preserve_ansi: false,
preserve_control_chars: false,
}
}
/// Process output according to config settings
///
/// Applies the configured stripping rules to the input data.
pub fn process_output(&self, data: &str) -> String {
if !self.preserve_ansi && !self.preserve_control_chars {
AnsiUtils::clean_for_processing(data)
} else if !self.preserve_ansi {
AnsiUtils::strip_ansi(data)
} else if !self.preserve_control_chars {
AnsiUtils::strip_control_chars(data)
} else {
data.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_ansi() {
let text = "\x1b[31mRed text\x1b[0m";
assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
}
#[test]
fn test_strip_ansi_multiple_codes() {
let text = "\x1b[1m\x1b[32mBold Green\x1b[0m Normal";
assert_eq!(AnsiUtils::strip_ansi(text), "Bold Green Normal");
}
#[test]
fn test_strip_control_chars() {
let text = "Hello\x00World\nNew line\tTab";
assert_eq!(
AnsiUtils::strip_control_chars(text),
"HelloWorld\nNew line\tTab"
);
}
#[test]
fn test_strip_control_chars_preserves_whitespace() {
let text = "Line1\nLine2\r\nLine3\tTabbed";
assert_eq!(
AnsiUtils::strip_control_chars(text),
"Line1\nLine2\r\nLine3\tTabbed"
);
}
#[test]
fn test_strip_all() {
let text = "\x1b[31mRed\x00text\x1b[0m";
assert_eq!(AnsiUtils::strip_all(text), "Redtext");
}
#[test]
fn test_ansi_config_default() {
let config = AnsiConfig::default();
let text = "\x1b[31mRed\x00text\x1b[0m";
assert_eq!(config.process_output(text), text);
}
#[test]
fn test_ansi_config_strip_all() {
let config = AnsiConfig::strip_all();
let text = "\x1b[31mRed\x00text\x1b[0m";
assert_eq!(config.process_output(text), "Redtext");
}
#[test]
fn test_ansi_config_strip_ansi_only() {
let config = AnsiConfig {
preserve_ansi: false,
preserve_control_chars: true,
};
let text = "\x1b[31mRed text\x1b[0m";
assert_eq!(config.process_output(text), "Red text");
}
#[test]
fn test_ansi_config_strip_control_only() {
let config = AnsiConfig {
preserve_ansi: true,
preserve_control_chars: false,
};
let text = "Hello\x00World";
assert_eq!(config.process_output(text), "HelloWorld");
}
}