1use crate::config::SerialConfig;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Command {
17 Help,
19 Quit,
21 ShowConfig,
23 ToggleDtr,
25 ToggleRts,
27 SendBreak,
29 SetBaud(u32),
31 OpenMenu,
33 ApplyConfig(SerialConfig),
40 SetDtrAbs(bool),
44 SetRtsAbs(bool),
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum ParseOutput {
52 None,
54 Data(u8),
56 Command(Command),
58}
59
60pub struct CommandKeyParser {
63 escape: u8,
64 state: State,
65}
66
67enum State {
68 Default,
69 AwaitingCommand,
70 AwaitingBaudDigits(String),
71}
72
73impl CommandKeyParser {
74 #[must_use]
77 pub const fn new(escape: u8) -> Self {
78 Self {
79 escape,
80 state: State::Default,
81 }
82 }
83
84 #[must_use]
86 pub const fn escape_byte(&self) -> u8 {
87 self.escape
88 }
89
90 pub fn feed(&mut self, byte: u8) -> ParseOutput {
101 match std::mem::replace(&mut self.state, State::Default) {
102 State::Default => {
103 if byte == self.escape {
104 self.state = State::AwaitingCommand;
105 ParseOutput::None
106 } else {
107 ParseOutput::Data(byte)
108 }
109 }
110 State::AwaitingCommand => self.handle_command_byte(byte),
111 State::AwaitingBaudDigits(buf) => self.handle_baud_byte(buf, byte),
112 }
113 }
114
115 fn handle_command_byte(&mut self, byte: u8) -> ParseOutput {
116 if byte == self.escape {
117 return ParseOutput::Data(self.escape);
119 }
120 match byte {
121 ESC_KEY => ParseOutput::None,
122 b'?' | b'h' => ParseOutput::Command(Command::Help),
123 CTRL_Q | CTRL_X => ParseOutput::Command(Command::Quit),
128 b'c' => ParseOutput::Command(Command::ShowConfig),
129 b't' => ParseOutput::Command(Command::ToggleDtr),
130 b'g' => ParseOutput::Command(Command::ToggleRts),
131 b'm' => ParseOutput::Command(Command::OpenMenu),
132 b'\\' => ParseOutput::Command(Command::SendBreak),
133 b'b' => {
134 self.state = State::AwaitingBaudDigits(String::new());
135 ParseOutput::None
136 }
137 _ => ParseOutput::None,
138 }
139 }
140
141 fn handle_baud_byte(&mut self, mut buf: String, byte: u8) -> ParseOutput {
142 match byte {
143 b'\r' | b'\n' => match buf.parse::<u32>() {
144 Ok(rate) if rate > 0 => ParseOutput::Command(Command::SetBaud(rate)),
145 _ => ParseOutput::None,
146 },
147 ESC_KEY => ParseOutput::None,
148 d if d.is_ascii_digit() => {
149 buf.push(d as char);
150 self.state = State::AwaitingBaudDigits(buf);
151 ParseOutput::None
152 }
153 _ => ParseOutput::None,
154 }
155 }
156}
157
158pub const DEFAULT_ESCAPE_BYTE: u8 = 0x01;
161
162impl Default for CommandKeyParser {
163 fn default() -> Self {
166 Self::new(DEFAULT_ESCAPE_BYTE)
167 }
168}
169
170const ESC_KEY: u8 = 0x1b;
171const CTRL_Q: u8 = 0x11;
173const CTRL_X: u8 = 0x18;
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 const ESC: u8 = 0x14; const fn parser() -> CommandKeyParser {
183 CommandKeyParser::new(ESC)
184 }
185
186 fn drive(p: &mut CommandKeyParser, bytes: &[u8]) -> Vec<ParseOutput> {
187 bytes.iter().map(|&b| p.feed(b)).collect()
188 }
189
190 #[test]
191 fn default_state_passes_bytes_through() {
192 let mut p = parser();
193 assert_eq!(
194 drive(&mut p, b"abc"),
195 vec![
196 ParseOutput::Data(b'a'),
197 ParseOutput::Data(b'b'),
198 ParseOutput::Data(b'c'),
199 ]
200 );
201 }
202
203 #[test]
204 fn escape_alone_produces_no_output() {
205 let mut p = parser();
206 assert_eq!(p.feed(ESC), ParseOutput::None);
207 }
208
209 #[test]
214 fn escape_then_ctrl_q_or_ctrl_x_emits_quit() {
215 for key in [0x11_u8, 0x18_u8] {
216 let mut p = parser();
217 assert_eq!(p.feed(ESC), ParseOutput::None);
218 assert_eq!(p.feed(key), ParseOutput::Command(Command::Quit));
219 }
220 }
221
222 #[test]
223 fn escape_then_lowercase_q_or_x_does_not_quit() {
224 for key in [b'q', b'x'] {
225 let mut p = parser();
226 assert_eq!(p.feed(ESC), ParseOutput::None);
227 assert_eq!(p.feed(key), ParseOutput::None);
229 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
231 }
232 }
233
234 #[test]
235 fn escape_then_help_keys_emit_help() {
236 for key in [b'?', b'h'] {
237 let mut p = parser();
238 p.feed(ESC);
239 assert_eq!(p.feed(key), ParseOutput::Command(Command::Help));
240 }
241 }
242
243 #[test]
244 fn escape_then_c_emits_show_config() {
245 let mut p = parser();
246 p.feed(ESC);
247 assert_eq!(p.feed(b'c'), ParseOutput::Command(Command::ShowConfig));
248 }
249
250 #[test]
251 fn escape_then_t_emits_toggle_dtr() {
252 let mut p = parser();
253 p.feed(ESC);
254 assert_eq!(p.feed(b't'), ParseOutput::Command(Command::ToggleDtr));
255 }
256
257 #[test]
258 fn escape_then_g_emits_toggle_rts() {
259 let mut p = parser();
260 p.feed(ESC);
261 assert_eq!(p.feed(b'g'), ParseOutput::Command(Command::ToggleRts));
262 }
263
264 #[test]
265 fn escape_then_backslash_emits_send_break() {
266 let mut p = parser();
267 p.feed(ESC);
268 assert_eq!(p.feed(b'\\'), ParseOutput::Command(Command::SendBreak));
269 }
270
271 #[test]
272 fn baud_change_collects_digits_and_emits_set_baud_on_cr() {
273 let mut p = parser();
274 p.feed(ESC);
275 assert_eq!(p.feed(b'b'), ParseOutput::None);
276 for &d in b"9600" {
277 assert_eq!(p.feed(d), ParseOutput::None);
278 }
279 assert_eq!(p.feed(b'\r'), ParseOutput::Command(Command::SetBaud(9600)));
280 }
281
282 #[test]
283 fn baud_change_lf_terminator_works_too() {
284 let mut p = parser();
285 p.feed(ESC);
286 p.feed(b'b');
287 for &d in b"115200" {
288 p.feed(d);
289 }
290 assert_eq!(
291 p.feed(b'\n'),
292 ParseOutput::Command(Command::SetBaud(115_200))
293 );
294 }
295
296 #[test]
297 fn baud_change_cancelled_by_esc_returns_to_default() {
298 let mut p = parser();
299 p.feed(ESC);
300 p.feed(b'b');
301 p.feed(b'9');
302 assert_eq!(p.feed(0x1b), ParseOutput::None);
303 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
305 }
306
307 #[test]
308 fn baud_change_cancelled_by_non_digit() {
309 let mut p = parser();
310 p.feed(ESC);
311 p.feed(b'b');
312 p.feed(b'9');
313 assert_eq!(p.feed(b'x'), ParseOutput::None);
314 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
315 }
316
317 #[test]
318 fn baud_change_with_empty_digits_is_dropped() {
319 let mut p = parser();
320 p.feed(ESC);
321 p.feed(b'b');
322 assert_eq!(p.feed(b'\r'), ParseOutput::None);
324 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
325 }
326
327 #[test]
328 fn double_escape_passes_escape_byte_through() {
329 let mut p = parser();
330 p.feed(ESC);
331 assert_eq!(p.feed(ESC), ParseOutput::Data(ESC));
332 }
333
334 #[test]
335 fn esc_in_command_state_cancels_quietly() {
336 let mut p = parser();
337 p.feed(ESC);
338 assert_eq!(p.feed(0x1b), ParseOutput::None);
339 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
340 }
341
342 #[test]
343 fn unknown_command_byte_silently_drops_and_resets() {
344 let mut p = parser();
345 p.feed(ESC);
346 assert_eq!(p.feed(b'z'), ParseOutput::None);
347 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
348 }
349
350 #[test]
351 fn pass_through_resumes_after_command() {
352 let mut p = parser();
353 p.feed(ESC);
354 assert_eq!(p.feed(0x18), ParseOutput::Command(Command::Quit));
356 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
357 }
358
359 #[test]
360 fn escape_byte_is_observable() {
361 assert_eq!(parser().escape_byte(), ESC);
362 }
363
364 #[test]
365 fn command_parser_recognizes_open_menu() {
366 let mut parser = CommandKeyParser::default();
367 assert_eq!(parser.feed(0x01), ParseOutput::None);
369 assert_eq!(parser.feed(b'm'), ParseOutput::Command(Command::OpenMenu));
370 }
371}