Skip to main content

rpi_tui/
keys.rs

1//! Keyboard input handling for terminal applications.
2//!
3//! Supports both legacy terminal sequences and Kitty keyboard protocol.
4//! See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
5
6use std::sync::atomic::{AtomicBool, Ordering};
7
8/// Global Kitty protocol state.
9static KITTY_PROTOCOL_ACTIVE: AtomicBool = AtomicBool::new(false);
10
11/// Set the global Kitty keyboard protocol state.
12pub fn set_kitty_protocol_active(active: bool) {
13    KITTY_PROTOCOL_ACTIVE.store(active, Ordering::SeqCst);
14}
15
16/// Query whether Kitty keyboard protocol is currently active.
17pub fn is_kitty_protocol_active() -> bool {
18    KITTY_PROTOCOL_ACTIVE.load(Ordering::SeqCst)
19}
20
21/// Key identifier type.
22pub type KeyId = String;
23
24/// Modifier names.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum Modifier {
27    Ctrl,
28    Shift,
29    Alt,
30    Super,
31}
32
33impl Modifier {
34    pub fn as_str(&self) -> &'static str {
35        match self {
36            Modifier::Ctrl => "ctrl",
37            Modifier::Shift => "shift",
38            Modifier::Alt => "alt",
39            Modifier::Super => "super",
40        }
41    }
42}
43
44/// A parsed key event.
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
46pub struct Key {
47    /// The base key (e.g., "a", "enter", "f1").
48    pub key: String,
49    /// Modifiers pressed.
50    pub modifiers: Vec<Modifier>,
51    /// Event type (press, release, repeat).
52    pub event_type: KeyEventType,
53}
54
55impl Key {
56    /// Create a new key with no modifiers.
57    pub fn new(key: impl Into<String>) -> Self {
58        Self {
59            key: key.into(),
60            modifiers: Vec::new(),
61            event_type: KeyEventType::Press,
62        }
63    }
64
65    /// Add a modifier.
66    pub fn with_modifier(mut self, modifier: Modifier) -> Self {
67        self.modifiers.push(modifier);
68        self
69    }
70
71    /// Set the event type.
72    pub fn with_event_type(mut self, event_type: KeyEventType) -> Self {
73        self.event_type = event_type;
74        self
75    }
76
77    /// Convert to key identifier string.
78    pub fn to_key_id(&self) -> KeyId {
79        let mut parts: Vec<&str> = self.modifiers.iter().map(|m| m.as_str()).collect();
80        parts.sort(); // Sort for consistency
81        parts.push(&self.key);
82        parts.join("+")
83    }
84
85    /// Check if this key matches a key identifier.
86    pub fn matches(&self, key_id: &str) -> bool {
87        self.to_key_id() == key_id
88    }
89}
90
91/// Key event type.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum KeyEventType {
94    Press,
95    Release,
96    Repeat,
97}
98
99/// Helper struct for creating typed key identifiers.
100pub struct KeyHelper;
101
102impl KeyHelper {
103    // Special keys
104    pub const ESCAPE: &'static str = "escape";
105    pub const ESC: &'static str = "esc";
106    pub const ENTER: &'static str = "enter";
107    pub const RETURN: &'static str = "return";
108    pub const TAB: &'static str = "tab";
109    pub const SPACE: &'static str = "space";
110    pub const BACKSPACE: &'static str = "backspace";
111    pub const DELETE: &'static str = "delete";
112    pub const INSERT: &'static str = "insert";
113    pub const CLEAR: &'static str = "clear";
114    pub const HOME: &'static str = "home";
115    pub const END: &'static str = "end";
116    pub const PAGE_UP: &'static str = "pageUp";
117    pub const PAGE_DOWN: &'static str = "pageDown";
118    pub const UP: &'static str = "up";
119    pub const DOWN: &'static str = "down";
120    pub const LEFT: &'static str = "left";
121    pub const RIGHT: &'static str = "right";
122
123    // Function keys
124    pub const F1: &'static str = "f1";
125    pub const F2: &'static str = "f2";
126    pub const F3: &'static str = "f3";
127    pub const F4: &'static str = "f4";
128    pub const F5: &'static str = "f5";
129    pub const F6: &'static str = "f6";
130    pub const F7: &'static str = "f7";
131    pub const F8: &'static str = "f8";
132    pub const F9: &'static str = "f9";
133    pub const F10: &'static str = "f10";
134    pub const F11: &'static str = "f11";
135    pub const F12: &'static str = "f12";
136
137    // Symbol keys
138    pub const BACKTICK: &'static str = "`";
139    pub const HYPHEN: &'static str = "-";
140    pub const EQUALS: &'static str = "=";
141    pub const LEFT_BRACKET: &'static str = "[";
142    pub const RIGHT_BRACKET: &'static str = "]";
143    pub const BACKSLASH: &'static str = "\\";
144    pub const SEMICOLON: &'static str = ";";
145    pub const QUOTE: &'static str = "'";
146    pub const COMMA: &'static str = ",";
147    pub const PERIOD: &'static str = ".";
148    pub const SLASH: &'static str = "/";
149
150    // Single modifier helpers
151    pub fn ctrl(key: &str) -> String {
152        format!("ctrl+{}", key)
153    }
154
155    pub fn shift(key: &str) -> String {
156        format!("shift+{}", key)
157    }
158
159    pub fn alt(key: &str) -> String {
160        format!("alt+{}", key)
161    }
162
163    pub fn super_key(key: &str) -> String {
164        format!("super+{}", key)
165    }
166
167    // Combined modifier helpers
168    pub fn ctrl_shift(key: &str) -> String {
169        format!("ctrl+shift+{}", key)
170    }
171
172    pub fn ctrl_alt(key: &str) -> String {
173        format!("ctrl+alt+{}", key)
174    }
175
176    pub fn alt_shift(key: &str) -> String {
177        format!("alt+shift+{}", key)
178    }
179}
180
181/// Symbol keys set.
182const SYMBOL_KEYS: &[&str] = &[
183    "`", "-", "=", "[", "]", "\\", ";", "'", ",", ".", "/", "!", "@", "#", "$", "%", "^", "&", "*",
184    "(", ")", "_", "+", "|", "~", "{", "}", ":", "<", ">", "?",
185];
186
187/// Check if a key is a symbol key.
188pub fn is_symbol_key(key: &str) -> bool {
189    SYMBOL_KEYS.contains(&key)
190}
191
192/// Modifier bit flags.
193const MOD_CTRL: u8 = 1;
194const MOD_SHIFT: u8 = 2;
195const MOD_ALT: u8 = 4;
196const MOD_SUPER: u8 = 8;
197#[allow(dead_code)]
198const LOCK_MASK: u8 = 64 + 128; // Caps Lock + Num Lock
199
200/// Parse a key event from terminal input data.
201/// Returns the parsed key if the data represents a key event.
202pub fn parse_key(data: &str) -> Option<Key> {
203    if data.is_empty() {
204        return None;
205    }
206
207    // Single character (including control characters)
208    if data.len() == 1 {
209        let c = data.chars().next()?;
210        return parse_single_char(c);
211    }
212
213    // Check for escape sequences
214    if data.starts_with('\x1b') {
215        return parse_escape_sequence(data);
216    }
217
218    None
219}
220
221/// Parse a single character key.
222fn parse_single_char(c: char) -> Option<Key> {
223    let code = c as u32;
224
225    // Control characters
226    match code {
227        0x00 => return Some(Key::new("ctrl+space")),
228        0x01..=0x07 => {
229            // Ctrl+A to Ctrl+G
230            let letter = (b'a' + (code - 1) as u8) as char;
231            return Some(Key::new(KeyHelper::ctrl(&letter.to_string())));
232        }
233        0x08 => return Some(Key::new(KeyHelper::BACKSPACE)), // Ctrl+H = Backspace
234        0x09 => return Some(Key::new(KeyHelper::TAB)),       // Ctrl+I = Tab
235        0x0a | 0x0d => return Some(Key::new(KeyHelper::ENTER)), // Ctrl+J/M = Enter
236        0x0b..=0x1a => {
237            // Ctrl+K to Ctrl+Z
238            let letter = (b'k' + (code - 0x0b) as u8) as char;
239            return Some(Key::new(KeyHelper::ctrl(&letter.to_string())));
240        }
241        0x1b => return Some(Key::new(KeyHelper::ESCAPE)),
242        0x7f => return Some(Key::new(KeyHelper::BACKSPACE)), // DEL
243        _ => {}
244    }
245
246    // Regular printable character
247    if c >= ' ' && c != '\x7f' {
248        let key_str = c.to_string();
249        if key_str.chars().all(|c| c.is_uppercase()) {
250            Some(Key::new(key_str.to_lowercase()).with_modifier(Modifier::Shift))
251        } else {
252            Some(Key::new(key_str))
253        }
254    } else {
255        None
256    }
257}
258
259/// Parse an escape sequence.
260fn parse_escape_sequence(data: &str) -> Option<Key> {
261    let bytes = data.as_bytes();
262
263    // CSI sequence: ESC [
264    if bytes.len() >= 3 && bytes[1] == b'[' {
265        return parse_csi_sequence(data);
266    }
267
268    // SS3 sequence: ESC O
269    if bytes.len() >= 3 && bytes[1] == b'O' {
270        return parse_ss3_sequence(data);
271    }
272
273    // Alt + key (ESC followed by single char)
274    if bytes.len() == 2 {
275        let c = bytes[1] as char;
276        let mut key = parse_single_char(c)?;
277        key.modifiers.push(Modifier::Alt);
278        return Some(key);
279    }
280
281    None
282}
283
284/// Parse a CSI sequence.
285fn parse_csi_sequence(data: &str) -> Option<Key> {
286    let bytes = data.as_bytes();
287
288    // Find the final byte (0x40-0x7E)
289    let final_idx = bytes.iter().rposition(|&b| (0x40..=0x7E).contains(&b))?;
290    let final_byte = bytes[final_idx];
291
292    // Parse parameters (between [ and final byte)
293    let params_str = &data[2..final_idx];
294    let params: Vec<u32> = params_str
295        .split(';')
296        .filter_map(|s| s.parse().ok())
297        .collect();
298
299    // Decode modifiers
300    let mut modifiers = Vec::new();
301    if params.len() >= 2 {
302        let mod_byte = params[1] as u8;
303        if mod_byte & MOD_CTRL != 0 {
304            modifiers.push(Modifier::Ctrl);
305        }
306        if mod_byte & MOD_SHIFT != 0 {
307            modifiers.push(Modifier::Shift);
308        }
309        if mod_byte & MOD_ALT != 0 {
310            modifiers.push(Modifier::Alt);
311        }
312        if mod_byte & MOD_SUPER != 0 {
313            modifiers.push(Modifier::Super);
314        }
315    }
316
317    // Determine event type (Kitty protocol)
318    let event_type = if params.len() >= 2 {
319        let event_type_code = params.get(0).copied().unwrap_or(1);
320        match event_type_code {
321            1 => KeyEventType::Press,
322            2 => KeyEventType::Repeat,
323            3 => KeyEventType::Release,
324            _ => KeyEventType::Press,
325        }
326    } else {
327        KeyEventType::Press
328    };
329
330    // Parse key based on final byte and parameters
331    let key = match final_byte {
332        b'~' => parse_csi_tilde_key(&params)?,
333        b'A' => Key::new(KeyHelper::UP),
334        b'B' => Key::new(KeyHelper::DOWN),
335        b'C' => Key::new(KeyHelper::RIGHT),
336        b'D' => Key::new(KeyHelper::LEFT),
337        b'E' => Key::new("begin"),
338        b'F' => Key::new(KeyHelper::END),
339        b'H' => Key::new(KeyHelper::HOME),
340        b'Z' => Key::new(KeyHelper::TAB).with_modifier(Modifier::Shift),
341        b'u' => {
342            // Kitty CSI-u: ESC [ code ; modifiers u
343            decode_kitty_printable(&params)?
344        }
345        _ => return None,
346    };
347
348    Some(key).map(|mut k| {
349        k.modifiers.extend(modifiers);
350        k.event_type = event_type;
351        k
352    })
353}
354
355/// Parse CSI tilde key (~ sequences).
356fn parse_csi_tilde_key(params: &[u32]) -> Option<Key> {
357    let code = params.first().copied().unwrap_or(0);
358    match code {
359        1 => Some(Key::new(KeyHelper::F1)),
360        2 => Some(Key::new(KeyHelper::INSERT)),
361        3 => Some(Key::new(KeyHelper::DELETE)),
362        4 => Some(Key::new(KeyHelper::END)),
363        5 => Some(Key::new(KeyHelper::PAGE_UP)),
364        6 => Some(Key::new(KeyHelper::PAGE_DOWN)),
365        7 => Some(Key::new(KeyHelper::HOME)),
366        8 => Some(Key::new(KeyHelper::END)),
367        11..=14 => Some(Key::new(KeyHelper::F1)),
368        15..=18 => Some(Key::new(KeyHelper::F5)),
369        19..=20 => Some(Key::new(KeyHelper::F9)),
370        21..=24 => Some(Key::new(KeyHelper::F11)),
371        _ => None,
372    }
373}
374
375/// Parse SS3 sequence (ESC O sequences).
376fn parse_ss3_sequence(data: &str) -> Option<Key> {
377    let final_char = data.chars().nth(2)?;
378    match final_char {
379        'A' => Some(Key::new(KeyHelper::UP)),
380        'B' => Some(Key::new(KeyHelper::DOWN)),
381        'C' => Some(Key::new(KeyHelper::RIGHT)),
382        'D' => Some(Key::new(KeyHelper::LEFT)),
383        'E' => Some(Key::new("begin")),
384        'F' => Some(Key::new(KeyHelper::END)),
385        'H' => Some(Key::new(KeyHelper::HOME)),
386        'P' => Some(Key::new(KeyHelper::F1)),
387        'Q' => Some(Key::new(KeyHelper::F2)),
388        'R' => Some(Key::new(KeyHelper::F3)),
389        'S' => Some(Key::new(KeyHelper::F4)),
390        _ => None,
391    }
392}
393
394/// Decode Kitty printable character (CSI-u).
395fn decode_kitty_printable(params: &[u32]) -> Option<Key> {
396    let code = params.first().copied().unwrap_or(0);
397
398    // Printable ASCII
399    if code >= 32 && code < 127 {
400        let c = code as u8 as char;
401        return Some(Key::new(c.to_string()));
402    }
403
404    // Decode special characters
405    match code {
406        57399 => Some(Key::new(KeyHelper::INSERT)),
407        57414 => Some(Key::new(KeyHelper::DELETE)),
408        57415 => Some(Key::new(KeyHelper::PAGE_UP)),
409        57416 => Some(Key::new(KeyHelper::PAGE_DOWN)),
410        57399..=57428 => {
411            // Function keys in Kitty
412            let offset = code - 57399;
413            let fn_key = match offset {
414                0 => "f1",
415                1 => "f2",
416                2 => "f3",
417                3 => "f4",
418                4 => "f5",
419                5 => "f6",
420                6 => "f7",
421                7 => "f8",
422                8 => "f9",
423                9 => "f10",
424                10 => "f11",
425                11 => "f12",
426                _ => return None,
427            };
428            Some(Key::new(fn_key))
429        }
430        _ => None,
431    }
432}
433
434/// Check if input data matches a key identifier.
435pub fn matches_key(data: &str, key_id: &str) -> bool {
436    parse_key(data).map(|k| k.matches(key_id)).unwrap_or(false)
437}
438
439/// Check if this is a key release event.
440pub fn is_key_release(data: &str) -> bool {
441    parse_key(data)
442        .map(|k| k.event_type == KeyEventType::Release)
443        .unwrap_or(false)
444}
445
446/// Check if this is a key repeat event.
447pub fn is_key_repeat(data: &str) -> bool {
448    parse_key(data)
449        .map(|k| k.event_type == KeyEventType::Repeat)
450        .unwrap_or(false)
451}
452
453/// Decode Kitty printable character from raw input.
454/// Returns the printable character if this is a Kitty CSI-u sequence.
455pub fn decode_kitty_printable_from_str(data: &str) -> Option<String> {
456    let bytes = data.as_bytes();
457
458    // Must be a CSI sequence ending with 'u'
459    if data.starts_with("\x1b[") && bytes.last()? == &b'u' {
460        // Parse parameters
461        let params_str = &data[2..data.len() - 1];
462        let params: Vec<u32> = params_str
463            .split(';')
464            .filter_map(|s| s.parse().ok())
465            .collect();
466
467        let code = params.first().copied().unwrap_or(0);
468
469        // Printable ASCII
470        if code >= 32 && code < 127 {
471            let c = code as u8 as char;
472            return Some(c.to_string());
473        }
474    }
475
476    None
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_single_char() {
485        let key = parse_key("a").unwrap();
486        assert_eq!(key.key, "a");
487        assert_eq!(key.to_key_id(), "a");
488    }
489
490    #[test]
491    fn test_ctrl_char() {
492        let key = parse_key("\x01").unwrap(); // Ctrl+A
493        assert_eq!(key.to_key_id(), "ctrl+a");
494    }
495
496    #[test]
497    fn test_escape() {
498        let key = parse_key("\x1b").unwrap();
499        assert_eq!(key.key, KeyHelper::ESCAPE);
500    }
501
502    #[test]
503    fn test_arrow_keys() {
504        let key = parse_key("\x1b[A").unwrap();
505        assert_eq!(key.key, KeyHelper::UP);
506
507        let key = parse_key("\x1b[B").unwrap();
508        assert_eq!(key.key, KeyHelper::DOWN);
509    }
510
511    #[test]
512    fn test_key_helper() {
513        assert_eq!(KeyHelper::ctrl("c"), "ctrl+c");
514        assert_eq!(KeyHelper::ctrl_shift("p"), "ctrl+shift+p");
515    }
516
517    #[test]
518    fn test_matches_key() {
519        assert!(matches_key("a", "a"));
520        assert!(matches_key("\x01", "ctrl+a"));
521        assert!(matches_key("\x1b[A", "up"));
522    }
523}