Skip to main content

rtcom_tui/
input.rs

1//! Key-event to byte-stream conversion.
2//!
3//! Translates `crossterm::event::KeyEvent`s into the byte sequences
4//! [`rtcom_core::command::CommandKeyParser`] expects. Matches picocom /
5//! minicom semantics: Enter is CR, Backspace is DEL (`0x7f`),
6//! Ctrl-char is `0x01..=0x1f`, plain character keys pass through their
7//! UTF-8 encoding.
8
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11use crate::modal::DialogAction;
12
13/// What the dispatcher decided to do with an inbound key event.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Dispatch {
16    /// Bytes to write to the serial device.
17    TxBytes(Vec<u8>),
18    /// Menu opened as a result of this event.
19    OpenedMenu,
20    /// Menu closed as a result of this event.
21    ClosedMenu,
22    /// User requested a clean quit.
23    Quit,
24    /// Dialog emitted a user-level action (apply-live, save-profile,
25    /// …). The outer runner interprets this and calls into
26    /// `rtcom-core` / `rtcom-config`.
27    Action(DialogAction),
28    /// No observable side effect (parser buffering, key swallowed
29    /// by the menu, etc.).
30    Noop,
31}
32
33/// Translate a crossterm [`KeyEvent`] into the raw byte sequence the
34/// remote device sees. Returns an empty `Vec` for events that do not
35/// correspond to a byte on the wire (e.g. modifier-only presses).
36///
37/// Semantics:
38/// - `Ctrl-<letter>` → `0x01..=0x1a` (e.g. Ctrl-A → `0x01`).
39/// - Plain [`KeyCode::Char`] → UTF-8 encoding of that character.
40/// - [`KeyCode::Enter`] → `CR` (`0x0d`), matching picocom's default
41///   "send CR on Enter" behaviour. Line-ending translation (CR → CRLF
42///   etc.) is the mapper's job (Issue #8), not ours.
43/// - [`KeyCode::Tab`] → `HT` (`0x09`).
44/// - [`KeyCode::Backspace`] → `DEL` (`0x7f`), again matching picocom.
45/// - [`KeyCode::Esc`] → `ESC` (`0x1b`).
46/// - Anything else (arrows, function keys, modifier-only) returns
47///   an empty `Vec`. T14+ can grow CSI sequences if a real use
48///   case surfaces.
49#[must_use]
50pub fn key_to_bytes(key: KeyEvent) -> Vec<u8> {
51    match (key.code, key.modifiers) {
52        // Ctrl-<letter>: 0x01..=0x1a
53        (KeyCode::Char(c), m) if m.contains(KeyModifiers::CONTROL) && c.is_ascii_alphabetic() => {
54            vec![(c.to_ascii_lowercase() as u8) - b'a' + 1]
55        }
56        // Plain printable char (possibly with Shift): UTF-8 encode
57        (KeyCode::Char(c), _) => {
58            let mut buf = [0u8; 4];
59            c.encode_utf8(&mut buf).as_bytes().to_vec()
60        }
61        (KeyCode::Enter, _) => vec![b'\r'],
62        (KeyCode::Tab, _) => vec![b'\t'],
63        (KeyCode::Backspace, _) => vec![0x7f],
64        (KeyCode::Esc, _) => vec![0x1b],
65        // Arrow keys, F-keys, etc. — emit nothing for T9 baseline.
66        // T12/T13 dialog navigation handles these inline (menu-open
67        // branch); the serial passthrough needs them only with
68        // CSI encoding, which T14+ can add if needed.
69        _ => Vec::new(),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn ctrl_a_is_0x01() {
79        let ev = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
80        assert_eq!(key_to_bytes(ev), vec![0x01]);
81    }
82
83    #[test]
84    fn plain_letter_is_ascii() {
85        let ev = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE);
86        assert_eq!(key_to_bytes(ev), vec![b'h']);
87    }
88
89    #[test]
90    fn enter_is_cr() {
91        let ev = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
92        assert_eq!(key_to_bytes(ev), vec![b'\r']);
93    }
94
95    #[test]
96    fn esc_is_0x1b() {
97        let ev = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
98        assert_eq!(key_to_bytes(ev), vec![0x1b]);
99    }
100}