Skip to main content

command_stream/
ansi.rs

1//! ANSI control character utilities for command-stream
2//!
3//! This module handles stripping and processing of ANSI escape codes
4//! and control characters from text output.
5
6/// ANSI control character utilities
7pub struct AnsiUtils;
8
9impl AnsiUtils {
10    /// Strip ANSI escape sequences from text
11    ///
12    /// Removes color codes, cursor movement, and other ANSI escape sequences
13    /// while preserving the actual text content.
14    ///
15    /// # Examples
16    ///
17    /// ```
18    /// use command_stream::ansi::AnsiUtils;
19    ///
20    /// let text = "\x1b[31mRed text\x1b[0m";
21    /// assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
22    /// ```
23    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    /// Strip control characters from text, preserving newlines, carriage returns, and tabs
29    ///
30    /// Removes control characters (ASCII 0x00-0x1F and 0x7F) except:
31    /// - Newlines (\n = 0x0A)
32    /// - Carriage returns (\r = 0x0D)
33    /// - Tabs (\t = 0x09)
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use command_stream::ansi::AnsiUtils;
39    ///
40    /// let text = "Hello\x00World\nNew line\tTab";
41    /// assert_eq!(AnsiUtils::strip_control_chars(text), "HelloWorld\nNew line\tTab");
42    /// ```
43    pub fn strip_control_chars(text: &str) -> String {
44        text.chars()
45            .filter(|c| {
46                // Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
47                !matches!(*c as u32,
48                    0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F
49                )
50            })
51            .collect()
52    }
53
54    /// Strip both ANSI sequences and control characters
55    ///
56    /// Combines `strip_ansi` and `strip_control_chars` for complete text cleaning.
57    pub fn strip_all(text: &str) -> String {
58        Self::strip_control_chars(&Self::strip_ansi(text))
59    }
60
61    /// Clean data for processing (strips ANSI and control chars)
62    ///
63    /// Alias for `strip_all` - provides semantic clarity when processing
64    /// data that needs to be cleaned for further processing.
65    pub fn clean_for_processing(data: &str) -> String {
66        Self::strip_all(data)
67    }
68}
69
70/// Configuration for ANSI handling
71///
72/// Controls how ANSI escape codes and control characters are processed
73/// in command output.
74#[derive(Debug, Clone)]
75pub struct AnsiConfig {
76    /// Whether to preserve ANSI escape sequences in output
77    pub preserve_ansi: bool,
78    /// Whether to preserve control characters in output
79    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    /// Create a new AnsiConfig that preserves everything (default)
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Create a config that strips all ANSI and control characters
98    pub fn strip_all() -> Self {
99        AnsiConfig {
100            preserve_ansi: false,
101            preserve_control_chars: false,
102        }
103    }
104
105    /// Process output according to config settings
106    ///
107    /// Applies the configured stripping rules to the input data.
108    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}