rust_expect/interact/
mode.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct InteractionMode {
8 pub local_echo: bool,
10 pub crlf: bool,
12 pub buffer_size: usize,
14 pub read_timeout: Duration,
16 pub exit_char: Option<u8>,
18 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), escape_char: Some(0x1e), }
32 }
33}
34
35impl InteractionMode {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 #[must_use]
44 pub const fn with_local_echo(mut self, echo: bool) -> Self {
45 self.local_echo = echo;
46 self
47 }
48
49 #[must_use]
51 pub const fn with_crlf(mut self, crlf: bool) -> Self {
52 self.crlf = crlf;
53 self
54 }
55
56 #[must_use]
58 pub const fn with_buffer_size(mut self, size: usize) -> Self {
59 self.buffer_size = size;
60 self
61 }
62
63 #[must_use]
65 pub const fn with_read_timeout(mut self, timeout: Duration) -> Self {
66 self.read_timeout = timeout;
67 self
68 }
69
70 #[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 #[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 #[must_use]
86 pub fn is_exit_char(&self, ch: u8) -> bool {
87 self.exit_char == Some(ch)
88 }
89
90 #[must_use]
92 pub fn is_escape_char(&self, ch: u8) -> bool {
93 self.escape_char == Some(ch)
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct InputFilter {
100 pub filter_chars: Vec<u8>,
102 pub allow_control: bool,
104 pub strip_high_bit: bool,
106}
107
108impl InputFilter {
109 #[must_use]
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 #[must_use]
117 pub fn filter(mut self, chars: &[u8]) -> Self {
118 self.filter_chars.extend_from_slice(chars);
119 self
120 }
121
122 #[must_use]
124 pub const fn with_control(mut self, allow: bool) -> Self {
125 self.allow_control = allow;
126 self
127 }
128
129 #[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#[derive(Debug, Clone, Default)]
144pub struct OutputFilter {
145 pub strip_ansi: bool,
147 pub normalize_newlines: bool,
149 pub strip_nulls: bool,
151}
152
153impl OutputFilter {
154 #[must_use]
156 pub fn new() -> Self {
157 Self::default()
158 }
159
160 #[must_use]
162 pub const fn with_strip_ansi(mut self, strip: bool) -> Self {
163 self.strip_ansi = strip;
164 self
165 }
166
167 #[must_use]
169 pub const fn with_normalize_newlines(mut self, normalize: bool) -> Self {
170 self.normalize_newlines = normalize;
171 self
172 }
173
174 #[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 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}