1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Command {
13 Help,
15 Quit,
17 ShowConfig,
19 ToggleDtr,
21 ToggleRts,
23 SendBreak,
25 SetBaud(u32),
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum ParseOutput {
32 None,
34 Data(u8),
36 Command(Command),
38}
39
40pub struct CommandKeyParser {
43 escape: u8,
44 state: State,
45}
46
47enum State {
48 Default,
49 AwaitingCommand,
50 AwaitingBaudDigits(String),
51}
52
53impl CommandKeyParser {
54 #[must_use]
57 pub const fn new(escape: u8) -> Self {
58 Self {
59 escape,
60 state: State::Default,
61 }
62 }
63
64 #[must_use]
66 pub const fn escape_byte(&self) -> u8 {
67 self.escape
68 }
69
70 pub fn feed(&mut self, byte: u8) -> ParseOutput {
81 match std::mem::replace(&mut self.state, State::Default) {
82 State::Default => {
83 if byte == self.escape {
84 self.state = State::AwaitingCommand;
85 ParseOutput::None
86 } else {
87 ParseOutput::Data(byte)
88 }
89 }
90 State::AwaitingCommand => self.handle_command_byte(byte),
91 State::AwaitingBaudDigits(buf) => self.handle_baud_byte(buf, byte),
92 }
93 }
94
95 fn handle_command_byte(&mut self, byte: u8) -> ParseOutput {
96 if byte == self.escape {
97 return ParseOutput::Data(self.escape);
99 }
100 match byte {
101 ESC_KEY => ParseOutput::None,
102 b'?' | b'h' => ParseOutput::Command(Command::Help),
103 CTRL_Q | CTRL_X => ParseOutput::Command(Command::Quit),
108 b'c' => ParseOutput::Command(Command::ShowConfig),
109 b't' => ParseOutput::Command(Command::ToggleDtr),
110 b'g' => ParseOutput::Command(Command::ToggleRts),
111 b'\\' => ParseOutput::Command(Command::SendBreak),
112 b'b' => {
113 self.state = State::AwaitingBaudDigits(String::new());
114 ParseOutput::None
115 }
116 _ => ParseOutput::None,
117 }
118 }
119
120 fn handle_baud_byte(&mut self, mut buf: String, byte: u8) -> ParseOutput {
121 match byte {
122 b'\r' | b'\n' => match buf.parse::<u32>() {
123 Ok(rate) if rate > 0 => ParseOutput::Command(Command::SetBaud(rate)),
124 _ => ParseOutput::None,
125 },
126 ESC_KEY => ParseOutput::None,
127 d if d.is_ascii_digit() => {
128 buf.push(d as char);
129 self.state = State::AwaitingBaudDigits(buf);
130 ParseOutput::None
131 }
132 _ => ParseOutput::None,
133 }
134 }
135}
136
137const ESC_KEY: u8 = 0x1b;
138const CTRL_Q: u8 = 0x11;
140const CTRL_X: u8 = 0x18;
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 const ESC: u8 = 0x14; const fn parser() -> CommandKeyParser {
150 CommandKeyParser::new(ESC)
151 }
152
153 fn drive(p: &mut CommandKeyParser, bytes: &[u8]) -> Vec<ParseOutput> {
154 bytes.iter().map(|&b| p.feed(b)).collect()
155 }
156
157 #[test]
158 fn default_state_passes_bytes_through() {
159 let mut p = parser();
160 assert_eq!(
161 drive(&mut p, b"abc"),
162 vec![
163 ParseOutput::Data(b'a'),
164 ParseOutput::Data(b'b'),
165 ParseOutput::Data(b'c'),
166 ]
167 );
168 }
169
170 #[test]
171 fn escape_alone_produces_no_output() {
172 let mut p = parser();
173 assert_eq!(p.feed(ESC), ParseOutput::None);
174 }
175
176 #[test]
181 fn escape_then_ctrl_q_or_ctrl_x_emits_quit() {
182 for key in [0x11_u8, 0x18_u8] {
183 let mut p = parser();
184 assert_eq!(p.feed(ESC), ParseOutput::None);
185 assert_eq!(p.feed(key), ParseOutput::Command(Command::Quit));
186 }
187 }
188
189 #[test]
190 fn escape_then_lowercase_q_or_x_does_not_quit() {
191 for key in [b'q', b'x'] {
192 let mut p = parser();
193 assert_eq!(p.feed(ESC), ParseOutput::None);
194 assert_eq!(p.feed(key), ParseOutput::None);
196 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
198 }
199 }
200
201 #[test]
202 fn escape_then_help_keys_emit_help() {
203 for key in [b'?', b'h'] {
204 let mut p = parser();
205 p.feed(ESC);
206 assert_eq!(p.feed(key), ParseOutput::Command(Command::Help));
207 }
208 }
209
210 #[test]
211 fn escape_then_c_emits_show_config() {
212 let mut p = parser();
213 p.feed(ESC);
214 assert_eq!(p.feed(b'c'), ParseOutput::Command(Command::ShowConfig));
215 }
216
217 #[test]
218 fn escape_then_t_emits_toggle_dtr() {
219 let mut p = parser();
220 p.feed(ESC);
221 assert_eq!(p.feed(b't'), ParseOutput::Command(Command::ToggleDtr));
222 }
223
224 #[test]
225 fn escape_then_g_emits_toggle_rts() {
226 let mut p = parser();
227 p.feed(ESC);
228 assert_eq!(p.feed(b'g'), ParseOutput::Command(Command::ToggleRts));
229 }
230
231 #[test]
232 fn escape_then_backslash_emits_send_break() {
233 let mut p = parser();
234 p.feed(ESC);
235 assert_eq!(p.feed(b'\\'), ParseOutput::Command(Command::SendBreak));
236 }
237
238 #[test]
239 fn baud_change_collects_digits_and_emits_set_baud_on_cr() {
240 let mut p = parser();
241 p.feed(ESC);
242 assert_eq!(p.feed(b'b'), ParseOutput::None);
243 for &d in b"9600" {
244 assert_eq!(p.feed(d), ParseOutput::None);
245 }
246 assert_eq!(p.feed(b'\r'), ParseOutput::Command(Command::SetBaud(9600)));
247 }
248
249 #[test]
250 fn baud_change_lf_terminator_works_too() {
251 let mut p = parser();
252 p.feed(ESC);
253 p.feed(b'b');
254 for &d in b"115200" {
255 p.feed(d);
256 }
257 assert_eq!(
258 p.feed(b'\n'),
259 ParseOutput::Command(Command::SetBaud(115_200))
260 );
261 }
262
263 #[test]
264 fn baud_change_cancelled_by_esc_returns_to_default() {
265 let mut p = parser();
266 p.feed(ESC);
267 p.feed(b'b');
268 p.feed(b'9');
269 assert_eq!(p.feed(0x1b), ParseOutput::None);
270 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
272 }
273
274 #[test]
275 fn baud_change_cancelled_by_non_digit() {
276 let mut p = parser();
277 p.feed(ESC);
278 p.feed(b'b');
279 p.feed(b'9');
280 assert_eq!(p.feed(b'x'), ParseOutput::None);
281 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
282 }
283
284 #[test]
285 fn baud_change_with_empty_digits_is_dropped() {
286 let mut p = parser();
287 p.feed(ESC);
288 p.feed(b'b');
289 assert_eq!(p.feed(b'\r'), ParseOutput::None);
291 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
292 }
293
294 #[test]
295 fn double_escape_passes_escape_byte_through() {
296 let mut p = parser();
297 p.feed(ESC);
298 assert_eq!(p.feed(ESC), ParseOutput::Data(ESC));
299 }
300
301 #[test]
302 fn esc_in_command_state_cancels_quietly() {
303 let mut p = parser();
304 p.feed(ESC);
305 assert_eq!(p.feed(0x1b), ParseOutput::None);
306 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
307 }
308
309 #[test]
310 fn unknown_command_byte_silently_drops_and_resets() {
311 let mut p = parser();
312 p.feed(ESC);
313 assert_eq!(p.feed(b'z'), ParseOutput::None);
314 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
315 }
316
317 #[test]
318 fn pass_through_resumes_after_command() {
319 let mut p = parser();
320 p.feed(ESC);
321 assert_eq!(p.feed(0x18), ParseOutput::Command(Command::Quit));
323 assert_eq!(p.feed(b'a'), ParseOutput::Data(b'a'));
324 }
325
326 #[test]
327 fn escape_byte_is_observable() {
328 assert_eq!(parser().escape_byte(), ESC);
329 }
330}