rust_expect/interact/
mode.rs

1//! Interaction mode handling.
2
3use std::time::Duration;
4
5/// Interaction mode configuration.
6#[derive(Debug, Clone)]
7pub struct InteractionMode {
8    /// Whether to echo input locally.
9    pub local_echo: bool,
10    /// Whether to translate CR to CRLF.
11    pub crlf: bool,
12    /// Input buffer size.
13    pub buffer_size: usize,
14    /// Read timeout.
15    pub read_timeout: Duration,
16    /// Exit character (e.g., Ctrl+]).
17    pub exit_char: Option<u8>,
18    /// Escape character for commands.
19    pub escape_char: Option<u8>,
20}
21
22impl Default for InteractionMode {
23    fn default() -> Self {
24        Self {
25            local_echo: false,
26            crlf: true,
27            buffer_size: 4096,
28            read_timeout: Duration::from_millis(100),
29            exit_char: Some(0x1d),   // Ctrl+]
30            escape_char: Some(0x1e), // Ctrl+^
31        }
32    }
33}
34
35impl InteractionMode {
36    /// Create a new mode with defaults.
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Enable local echo.
43    #[must_use]
44    pub const fn with_local_echo(mut self, echo: bool) -> Self {
45        self.local_echo = echo;
46        self
47    }
48
49    /// Enable CRLF translation.
50    #[must_use]
51    pub const fn with_crlf(mut self, crlf: bool) -> Self {
52        self.crlf = crlf;
53        self
54    }
55
56    /// Set buffer size.
57    #[must_use]
58    pub const fn with_buffer_size(mut self, size: usize) -> Self {
59        self.buffer_size = size;
60        self
61    }
62
63    /// Set read timeout.
64    #[must_use]
65    pub const fn with_read_timeout(mut self, timeout: Duration) -> Self {
66        self.read_timeout = timeout;
67        self
68    }
69
70    /// Set exit character.
71    #[must_use]
72    pub const fn with_exit_char(mut self, ch: Option<u8>) -> Self {
73        self.exit_char = ch;
74        self
75    }
76
77    /// Set escape character.
78    #[must_use]
79    pub const fn with_escape_char(mut self, ch: Option<u8>) -> Self {
80        self.escape_char = ch;
81        self
82    }
83
84    /// Check if a character is the exit character.
85    #[must_use]
86    pub fn is_exit_char(&self, ch: u8) -> bool {
87        self.exit_char == Some(ch)
88    }
89
90    /// Check if a character is the escape character.
91    #[must_use]
92    pub fn is_escape_char(&self, ch: u8) -> bool {
93        self.escape_char == Some(ch)
94    }
95}
96
97/// Input filter for processing user input.
98#[derive(Debug, Clone, Default)]
99pub struct InputFilter {
100    /// Characters to filter out.
101    pub filter_chars: Vec<u8>,
102    /// Whether to allow control characters.
103    pub allow_control: bool,
104    /// Whether to strip high bit.
105    pub strip_high_bit: bool,
106}
107
108impl InputFilter {
109    /// Create a new filter.
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Add characters to filter.
116    #[must_use]
117    pub fn filter(mut self, chars: &[u8]) -> Self {
118        self.filter_chars.extend_from_slice(chars);
119        self
120    }
121
122    /// Allow control characters.
123    #[must_use]
124    pub const fn with_control(mut self, allow: bool) -> Self {
125        self.allow_control = allow;
126        self
127    }
128
129    /// Apply filter to input.
130    #[must_use]
131    pub fn apply(&self, input: &[u8]) -> Vec<u8> {
132        input
133            .iter()
134            .copied()
135            .filter(|&b| !self.filter_chars.contains(&b))
136            .filter(|&b| self.allow_control || b >= 0x20 || b == b'\r' || b == b'\n' || b == b'\t')
137            .map(|b| if self.strip_high_bit { b & 0x7f } else { b })
138            .collect()
139    }
140}
141
142/// Output filter for processing session output.
143#[derive(Debug, Clone, Default)]
144pub struct OutputFilter {
145    /// Whether to strip ANSI sequences.
146    pub strip_ansi: bool,
147    /// Whether to convert CRLF to LF.
148    pub normalize_newlines: bool,
149    /// Whether to strip null bytes.
150    pub strip_nulls: bool,
151}
152
153impl OutputFilter {
154    /// Create a new filter.
155    #[must_use]
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Strip ANSI sequences.
161    #[must_use]
162    pub const fn with_strip_ansi(mut self, strip: bool) -> Self {
163        self.strip_ansi = strip;
164        self
165    }
166
167    /// Normalize newlines.
168    #[must_use]
169    pub const fn with_normalize_newlines(mut self, normalize: bool) -> Self {
170        self.normalize_newlines = normalize;
171        self
172    }
173
174    /// Apply filter to output.
175    #[must_use]
176    pub fn apply(&self, output: &[u8]) -> Vec<u8> {
177        let mut result: Vec<u8> = output
178            .iter()
179            .copied()
180            .filter(|&b| !self.strip_nulls || b != 0)
181            .collect();
182
183        if self.normalize_newlines {
184            // Replace CRLF with LF
185            let mut i = 0;
186            let mut normalized = Vec::with_capacity(result.len());
187            while i < result.len() {
188                if i + 1 < result.len() && result[i] == b'\r' && result[i + 1] == b'\n' {
189                    normalized.push(b'\n');
190                    i += 2;
191                } else {
192                    normalized.push(result[i]);
193                    i += 1;
194                }
195            }
196            result = normalized;
197        }
198
199        if self.strip_ansi {
200            result = crate::util::bytes::strip_ansi(&result);
201        }
202
203        result
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn mode_defaults() {
213        let mode = InteractionMode::new();
214        assert!(!mode.local_echo);
215        assert!(mode.crlf);
216    }
217
218    #[test]
219    fn input_filter() {
220        let filter = InputFilter::new().filter(b"x");
221        let result = filter.apply(b"text");
222        assert_eq!(result, b"tet");
223    }
224
225    #[test]
226    fn output_normalize_newlines() {
227        let filter = OutputFilter::new().with_normalize_newlines(true);
228        let result = filter.apply(b"line1\r\nline2\r\n");
229        assert_eq!(result, b"line1\nline2\n");
230    }
231}