Skip to main content

par_term_keybindings/
parser.rs

1//! Key combination parser.
2//!
3//! Parses human-readable key strings like "Ctrl+Shift+B" into KeyCombo structs.
4//! Also supports physical key codes for language-agnostic bindings (e.g., "Ctrl+[KeyZ]").
5
6use std::fmt;
7use winit::keyboard::{KeyCode, NamedKey};
8
9/// Error type for key parsing failures.
10#[derive(Debug, Clone)]
11pub struct ParseError(String);
12
13impl fmt::Display for ParseError {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        write!(f, "{}", self.0)
16    }
17}
18
19impl std::error::Error for ParseError {}
20
21/// Set of active modifiers for a key combination.
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
23pub struct Modifiers {
24    pub ctrl: bool,
25    pub alt: bool,
26    pub shift: bool,
27    pub super_key: bool,
28    /// If true, this represents CmdOrCtrl (Cmd on macOS, Ctrl elsewhere)
29    pub cmd_or_ctrl: bool,
30}
31
32/// A parsed key combination (modifiers + key).
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct KeyCombo {
35    pub modifiers: Modifiers,
36    pub key: ParsedKey,
37}
38
39impl fmt::Display for KeyCombo {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        let mut parts = Vec::new();
42
43        if self.modifiers.cmd_or_ctrl {
44            parts.push("CmdOrCtrl".to_string());
45        }
46        if self.modifiers.ctrl {
47            parts.push("Ctrl".to_string());
48        }
49        if self.modifiers.alt {
50            parts.push("Alt".to_string());
51        }
52        if self.modifiers.shift {
53            parts.push("Shift".to_string());
54        }
55        if self.modifiers.super_key {
56            parts.push("Super".to_string());
57        }
58
59        match &self.key {
60            ParsedKey::Character(c) => parts.push(c.to_string()),
61            ParsedKey::Named(n) => parts.push(format!("{:?}", n)),
62            ParsedKey::Physical(k) => parts.push(format!("[{:?}]", k)),
63        }
64
65        write!(f, "{}", parts.join("+"))
66    }
67}
68
69/// The actual key (either a character or a named key).
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub enum ParsedKey {
72    /// A single character key (e.g., 'a', 'B', '1')
73    Character(char),
74    /// A named key (e.g., F1, Enter, Escape)
75    Named(NamedKey),
76    /// A physical key code (e.g., KeyZ, KeyA) for language-agnostic bindings
77    /// This matches by key position rather than character produced.
78    Physical(KeyCode),
79}
80
81/// Parse a key combination string into a KeyCombo.
82///
83/// Supported format: "Modifier+Modifier+Key"
84///
85/// Modifiers:
86/// - `Ctrl`, `Control` - Control key
87/// - `Alt`, `Option` - Alt/Option key
88/// - `Shift` - Shift key
89/// - `Super`, `Cmd`, `Command`, `Meta`, `Win` - Super/Cmd key
90/// - `CmdOrCtrl` - Cmd on macOS, Ctrl on other platforms
91///
92/// Keys:
93/// - Single characters: `A`, `B`, `1`, etc.
94/// - Named keys: `F1`-`F12`, `Enter`, `Escape`, `Space`, `Tab`, etc.
95pub fn parse_key_combo(s: &str) -> Result<KeyCombo, ParseError> {
96    let parts: Vec<&str> = s.split('+').map(str::trim).collect();
97
98    if parts.is_empty() {
99        return Err(ParseError("Empty key combination".to_string()));
100    }
101
102    let mut modifiers = Modifiers::default();
103    let mut key_part = None;
104
105    for (i, part) in parts.iter().enumerate() {
106        let is_last = i == parts.len() - 1;
107        let part_lower = part.to_lowercase();
108
109        // Check if this is a modifier
110        let is_modifier = match part_lower.as_str() {
111            "ctrl" | "control" => {
112                modifiers.ctrl = true;
113                true
114            }
115            "alt" | "option" => {
116                modifiers.alt = true;
117                true
118            }
119            "shift" => {
120                modifiers.shift = true;
121                true
122            }
123            "super" | "cmd" | "command" | "meta" | "win" => {
124                modifiers.super_key = true;
125                true
126            }
127            "cmdorctrl" => {
128                modifiers.cmd_or_ctrl = true;
129                true
130            }
131            _ => false,
132        };
133
134        if !is_modifier {
135            if key_part.is_some() {
136                return Err(ParseError(format!(
137                    "Multiple keys specified: already have key, found '{}'",
138                    part
139                )));
140            }
141            key_part = Some(*part);
142        } else if is_last {
143            // Last part is a modifier with no key - invalid
144            return Err(ParseError(
145                "Key combination ends with modifier, no key specified".to_string(),
146            ));
147        }
148    }
149
150    let key_str = key_part.ok_or_else(|| ParseError("No key specified".to_string()))?;
151    let key = parse_key(key_str)?;
152
153    Ok(KeyCombo { modifiers, key })
154}
155
156/// Parse a key string into a ParsedKey.
157fn parse_key(s: &str) -> Result<ParsedKey, ParseError> {
158    // Check for physical key syntax: [KeyCode] (e.g., [KeyZ], [KeyA])
159    if s.starts_with('[') && s.ends_with(']') {
160        let code_str = &s[1..s.len() - 1];
161        if let Some(code) = parse_physical_key_code(code_str) {
162            return Ok(ParsedKey::Physical(code));
163        }
164        return Err(ParseError(format!(
165            "Unknown physical key code: '{}'",
166            code_str
167        )));
168    }
169
170    // Try named keys first (case-insensitive)
171    if let Some(named) = parse_named_key(s) {
172        return Ok(ParsedKey::Named(named));
173    }
174
175    // Single character
176    let chars: Vec<char> = s.chars().collect();
177    if chars.len() == 1 {
178        return Ok(ParsedKey::Character(chars[0].to_ascii_uppercase()));
179    }
180
181    Err(ParseError(format!("Unknown key: '{}'", s)))
182}
183
184/// Parse a physical key code string into a KeyCode.
185/// Supports common key code names like "KeyA", "KeyZ", "Digit0", etc.
186fn parse_physical_key_code(s: &str) -> Option<KeyCode> {
187    match s.to_lowercase().as_str() {
188        // Letter keys
189        "keya" => Some(KeyCode::KeyA),
190        "keyb" => Some(KeyCode::KeyB),
191        "keyc" => Some(KeyCode::KeyC),
192        "keyd" => Some(KeyCode::KeyD),
193        "keye" => Some(KeyCode::KeyE),
194        "keyf" => Some(KeyCode::KeyF),
195        "keyg" => Some(KeyCode::KeyG),
196        "keyh" => Some(KeyCode::KeyH),
197        "keyi" => Some(KeyCode::KeyI),
198        "keyj" => Some(KeyCode::KeyJ),
199        "keyk" => Some(KeyCode::KeyK),
200        "keyl" => Some(KeyCode::KeyL),
201        "keym" => Some(KeyCode::KeyM),
202        "keyn" => Some(KeyCode::KeyN),
203        "keyo" => Some(KeyCode::KeyO),
204        "keyp" => Some(KeyCode::KeyP),
205        "keyq" => Some(KeyCode::KeyQ),
206        "keyr" => Some(KeyCode::KeyR),
207        "keys" => Some(KeyCode::KeyS),
208        "keyt" => Some(KeyCode::KeyT),
209        "keyu" => Some(KeyCode::KeyU),
210        "keyv" => Some(KeyCode::KeyV),
211        "keyw" => Some(KeyCode::KeyW),
212        "keyx" => Some(KeyCode::KeyX),
213        "keyy" => Some(KeyCode::KeyY),
214        "keyz" => Some(KeyCode::KeyZ),
215
216        // Number row
217        "digit0" => Some(KeyCode::Digit0),
218        "digit1" => Some(KeyCode::Digit1),
219        "digit2" => Some(KeyCode::Digit2),
220        "digit3" => Some(KeyCode::Digit3),
221        "digit4" => Some(KeyCode::Digit4),
222        "digit5" => Some(KeyCode::Digit5),
223        "digit6" => Some(KeyCode::Digit6),
224        "digit7" => Some(KeyCode::Digit7),
225        "digit8" => Some(KeyCode::Digit8),
226        "digit9" => Some(KeyCode::Digit9),
227
228        // Punctuation/symbols by position
229        "minus" => Some(KeyCode::Minus),
230        "equal" => Some(KeyCode::Equal),
231        "bracketleft" => Some(KeyCode::BracketLeft),
232        "bracketright" => Some(KeyCode::BracketRight),
233        "backslash" => Some(KeyCode::Backslash),
234        "semicolon" => Some(KeyCode::Semicolon),
235        "quote" => Some(KeyCode::Quote),
236        "backquote" => Some(KeyCode::Backquote),
237        "comma" => Some(KeyCode::Comma),
238        "period" => Some(KeyCode::Period),
239        "slash" => Some(KeyCode::Slash),
240
241        // Function keys
242        "f1" => Some(KeyCode::F1),
243        "f2" => Some(KeyCode::F2),
244        "f3" => Some(KeyCode::F3),
245        "f4" => Some(KeyCode::F4),
246        "f5" => Some(KeyCode::F5),
247        "f6" => Some(KeyCode::F6),
248        "f7" => Some(KeyCode::F7),
249        "f8" => Some(KeyCode::F8),
250        "f9" => Some(KeyCode::F9),
251        "f10" => Some(KeyCode::F10),
252        "f11" => Some(KeyCode::F11),
253        "f12" => Some(KeyCode::F12),
254
255        // Navigation keys
256        "arrowup" => Some(KeyCode::ArrowUp),
257        "arrowdown" => Some(KeyCode::ArrowDown),
258        "arrowleft" => Some(KeyCode::ArrowLeft),
259        "arrowright" => Some(KeyCode::ArrowRight),
260        "home" => Some(KeyCode::Home),
261        "end" => Some(KeyCode::End),
262        "pageup" => Some(KeyCode::PageUp),
263        "pagedown" => Some(KeyCode::PageDown),
264        "insert" => Some(KeyCode::Insert),
265        "delete" => Some(KeyCode::Delete),
266
267        // Special keys
268        "enter" => Some(KeyCode::Enter),
269        "escape" => Some(KeyCode::Escape),
270        "space" => Some(KeyCode::Space),
271        "tab" => Some(KeyCode::Tab),
272        "backspace" => Some(KeyCode::Backspace),
273
274        _ => None,
275    }
276}
277
278/// Parse a named key string into a NamedKey.
279fn parse_named_key(s: &str) -> Option<NamedKey> {
280    match s.to_lowercase().as_str() {
281        // Function keys
282        "f1" => Some(NamedKey::F1),
283        "f2" => Some(NamedKey::F2),
284        "f3" => Some(NamedKey::F3),
285        "f4" => Some(NamedKey::F4),
286        "f5" => Some(NamedKey::F5),
287        "f6" => Some(NamedKey::F6),
288        "f7" => Some(NamedKey::F7),
289        "f8" => Some(NamedKey::F8),
290        "f9" => Some(NamedKey::F9),
291        "f10" => Some(NamedKey::F10),
292        "f11" => Some(NamedKey::F11),
293        "f12" => Some(NamedKey::F12),
294
295        // Common named keys
296        "enter" | "return" => Some(NamedKey::Enter),
297        "escape" | "esc" => Some(NamedKey::Escape),
298        "space" => Some(NamedKey::Space),
299        "tab" => Some(NamedKey::Tab),
300        "backspace" => Some(NamedKey::Backspace),
301        "delete" | "del" => Some(NamedKey::Delete),
302        "insert" | "ins" => Some(NamedKey::Insert),
303        "home" => Some(NamedKey::Home),
304        "end" => Some(NamedKey::End),
305        "pageup" | "pgup" => Some(NamedKey::PageUp),
306        "pagedown" | "pgdn" => Some(NamedKey::PageDown),
307
308        // Arrow keys
309        "up" | "arrowup" => Some(NamedKey::ArrowUp),
310        "down" | "arrowdown" => Some(NamedKey::ArrowDown),
311        "left" | "arrowleft" => Some(NamedKey::ArrowLeft),
312        "right" | "arrowright" => Some(NamedKey::ArrowRight),
313
314        _ => None,
315    }
316}
317
318/// Convert a parsed `KeyCombo` into terminal byte sequence(s).
319///
320/// Maps key combinations to their terminal escape sequences:
321/// - Ctrl+Character: control code (char - 'a' + 1)
322/// - Named keys (Enter, Tab, arrows, F-keys): standard escape sequences
323/// - Plain characters: UTF-8 bytes
324/// - Alt+key: ESC prefix + key bytes
325pub fn key_combo_to_bytes(combo: &KeyCombo) -> Result<Vec<u8>, String> {
326    let has_ctrl = combo.modifiers.ctrl || combo.modifiers.cmd_or_ctrl;
327    let has_alt = combo.modifiers.alt;
328
329    match &combo.key {
330        ParsedKey::Character(c) => {
331            if has_ctrl {
332                // Control codes: Ctrl+A = 0x01, Ctrl+C = 0x03, etc.
333                let upper = c.to_ascii_uppercase();
334                if upper.is_ascii_uppercase() {
335                    let code = upper as u8 - b'A' + 1;
336                    let bytes = vec![code];
337                    if has_alt {
338                        // Alt+Ctrl+Key = ESC + control code
339                        let mut result = vec![0x1b];
340                        result.extend_from_slice(&bytes);
341                        Ok(result)
342                    } else {
343                        Ok(bytes)
344                    }
345                } else {
346                    Err(format!("Cannot compute Ctrl code for '{}'", c))
347                }
348            } else if has_alt {
349                // Alt+Key = ESC + key
350                let mut bytes = vec![0x1b];
351                let mut buf = [0u8; 4];
352                let encoded = c.encode_utf8(&mut buf);
353                bytes.extend_from_slice(encoded.as_bytes());
354                Ok(bytes)
355            } else {
356                // Plain character
357                let mut buf = [0u8; 4];
358                let encoded = c.encode_utf8(&mut buf);
359                Ok(encoded.as_bytes().to_vec())
360            }
361        }
362        ParsedKey::Named(named) => {
363            let seq: &[u8] = match named {
364                NamedKey::Enter => b"\r",
365                NamedKey::Tab => b"\t",
366                NamedKey::Space => b" ",
367                NamedKey::Backspace => b"\x7f",
368                NamedKey::Escape => b"\x1b",
369                NamedKey::Insert => b"\x1b[2~",
370                NamedKey::Delete => b"\x1b[3~",
371                NamedKey::ArrowUp => b"\x1b[A",
372                NamedKey::ArrowDown => b"\x1b[B",
373                NamedKey::ArrowRight => b"\x1b[C",
374                NamedKey::ArrowLeft => b"\x1b[D",
375                NamedKey::Home => b"\x1b[H",
376                NamedKey::End => b"\x1b[F",
377                NamedKey::PageUp => b"\x1b[5~",
378                NamedKey::PageDown => b"\x1b[6~",
379                NamedKey::F1 => b"\x1bOP",
380                NamedKey::F2 => b"\x1bOQ",
381                NamedKey::F3 => b"\x1bOR",
382                NamedKey::F4 => b"\x1bOS",
383                NamedKey::F5 => b"\x1b[15~",
384                NamedKey::F6 => b"\x1b[17~",
385                NamedKey::F7 => b"\x1b[18~",
386                NamedKey::F8 => b"\x1b[19~",
387                NamedKey::F9 => b"\x1b[20~",
388                NamedKey::F10 => b"\x1b[21~",
389                NamedKey::F11 => b"\x1b[23~",
390                NamedKey::F12 => b"\x1b[24~",
391                _ => return Err(format!("Unsupported named key: {:?}", named)),
392            };
393            let bytes = seq.to_vec();
394            if has_alt {
395                let mut result = vec![0x1b];
396                result.extend_from_slice(&bytes);
397                Ok(result)
398            } else {
399                Ok(bytes)
400            }
401        }
402        ParsedKey::Physical(_code) => {
403            Err("Physical key codes cannot be converted to bytes without a keyboard layout".into())
404        }
405    }
406}
407
408/// Parse a key sequence string into a list of byte sequences.
409///
410/// The input string contains whitespace-separated key combos.
411/// Each key combo is parsed with `parse_key_combo()` and converted to bytes.
412///
413/// Example: "Up Up Down Down" → four arrow key escape sequences
414/// Example: "Ctrl+C" → single \x03 byte
415pub fn parse_key_sequence(keys: &str) -> Result<Vec<Vec<u8>>, String> {
416    let trimmed = keys.trim();
417    if trimmed.is_empty() {
418        return Err("Empty key sequence".to_string());
419    }
420
421    let parts: Vec<&str> = trimmed.split_whitespace().collect();
422    let mut result = Vec::with_capacity(parts.len());
423
424    for part in parts {
425        let combo = parse_key_combo(part).map_err(|e| format!("'{}': {}", part, e))?;
426        let bytes = key_combo_to_bytes(&combo)?;
427        result.push(bytes);
428    }
429
430    Ok(result)
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_simple_key() {
439        let combo = parse_key_combo("A").unwrap();
440        assert!(!combo.modifiers.ctrl);
441        assert!(!combo.modifiers.shift);
442        assert_eq!(combo.key, ParsedKey::Character('A'));
443    }
444
445    #[test]
446    fn test_ctrl_key() {
447        let combo = parse_key_combo("Ctrl+A").unwrap();
448        assert!(combo.modifiers.ctrl);
449        assert!(!combo.modifiers.shift);
450        assert_eq!(combo.key, ParsedKey::Character('A'));
451    }
452
453    #[test]
454    fn test_ctrl_shift_key() {
455        let combo = parse_key_combo("Ctrl+Shift+B").unwrap();
456        assert!(combo.modifiers.ctrl);
457        assert!(combo.modifiers.shift);
458        assert_eq!(combo.key, ParsedKey::Character('B'));
459    }
460
461    #[test]
462    fn test_cmd_or_ctrl() {
463        let combo = parse_key_combo("CmdOrCtrl+Shift+B").unwrap();
464        assert!(combo.modifiers.cmd_or_ctrl);
465        assert!(combo.modifiers.shift);
466        assert!(!combo.modifiers.ctrl);
467        assert_eq!(combo.key, ParsedKey::Character('B'));
468    }
469
470    #[test]
471    fn test_function_key() {
472        let combo = parse_key_combo("F5").unwrap();
473        assert!(!combo.modifiers.ctrl);
474        assert_eq!(combo.key, ParsedKey::Named(NamedKey::F5));
475    }
476
477    #[test]
478    fn test_ctrl_function_key() {
479        let combo = parse_key_combo("Ctrl+F12").unwrap();
480        assert!(combo.modifiers.ctrl);
481        assert_eq!(combo.key, ParsedKey::Named(NamedKey::F12));
482    }
483
484    #[test]
485    fn test_case_insensitive() {
486        let combo = parse_key_combo("ctrl+shift+a").unwrap();
487        assert!(combo.modifiers.ctrl);
488        assert!(combo.modifiers.shift);
489        assert_eq!(combo.key, ParsedKey::Character('A'));
490    }
491
492    #[test]
493    fn test_modifier_aliases() {
494        // Control alias
495        let combo = parse_key_combo("Control+A").unwrap();
496        assert!(combo.modifiers.ctrl);
497
498        // Option alias
499        let combo = parse_key_combo("Option+A").unwrap();
500        assert!(combo.modifiers.alt);
501
502        // Command aliases
503        let combo = parse_key_combo("Cmd+A").unwrap();
504        assert!(combo.modifiers.super_key);
505
506        let combo = parse_key_combo("Command+A").unwrap();
507        assert!(combo.modifiers.super_key);
508
509        let combo = parse_key_combo("Meta+A").unwrap();
510        assert!(combo.modifiers.super_key);
511    }
512
513    #[test]
514    fn test_named_key_aliases() {
515        let combo = parse_key_combo("Enter").unwrap();
516        assert_eq!(combo.key, ParsedKey::Named(NamedKey::Enter));
517
518        let combo = parse_key_combo("Return").unwrap();
519        assert_eq!(combo.key, ParsedKey::Named(NamedKey::Enter));
520
521        let combo = parse_key_combo("Esc").unwrap();
522        assert_eq!(combo.key, ParsedKey::Named(NamedKey::Escape));
523
524        let combo = parse_key_combo("PgUp").unwrap();
525        assert_eq!(combo.key, ParsedKey::Named(NamedKey::PageUp));
526    }
527
528    #[test]
529    fn test_invalid_empty() {
530        assert!(parse_key_combo("").is_err());
531    }
532
533    #[test]
534    fn test_invalid_modifier_only() {
535        assert!(parse_key_combo("Ctrl").is_err());
536        assert!(parse_key_combo("Ctrl+Shift").is_err());
537    }
538
539    #[test]
540    fn test_invalid_unknown_key() {
541        assert!(parse_key_combo("Ctrl+UnknownKey").is_err());
542    }
543
544    #[test]
545    fn test_display() {
546        let combo = parse_key_combo("Ctrl+Shift+B").unwrap();
547        let display = format!("{}", combo);
548        assert!(display.contains("Ctrl"));
549        assert!(display.contains("Shift"));
550        assert!(display.contains("B"));
551    }
552
553    #[test]
554    fn test_physical_key() {
555        let combo = parse_key_combo("Ctrl+[KeyZ]").unwrap();
556        assert!(combo.modifiers.ctrl);
557        assert_eq!(combo.key, ParsedKey::Physical(KeyCode::KeyZ));
558    }
559
560    #[test]
561    fn test_physical_key_case_insensitive() {
562        let combo = parse_key_combo("Ctrl+[keya]").unwrap();
563        assert!(combo.modifiers.ctrl);
564        assert_eq!(combo.key, ParsedKey::Physical(KeyCode::KeyA));
565    }
566
567    #[test]
568    fn test_physical_key_display() {
569        let combo = parse_key_combo("Ctrl+[KeyZ]").unwrap();
570        let display = format!("{}", combo);
571        assert!(display.contains("Ctrl"));
572        assert!(display.contains("[KeyZ]"));
573    }
574
575    #[test]
576    fn test_invalid_physical_key() {
577        assert!(parse_key_combo("Ctrl+[Unknown]").is_err());
578    }
579
580    // Tests for key_combo_to_bytes and parse_key_sequence
581
582    #[test]
583    fn test_key_combo_to_bytes_enter() {
584        let combo = parse_key_combo("Enter").unwrap();
585        let bytes = key_combo_to_bytes(&combo).unwrap();
586        assert_eq!(bytes, b"\r");
587    }
588
589    #[test]
590    fn test_key_combo_to_bytes_tab() {
591        let combo = parse_key_combo("Tab").unwrap();
592        let bytes = key_combo_to_bytes(&combo).unwrap();
593        assert_eq!(bytes, b"\t");
594    }
595
596    #[test]
597    fn test_key_combo_to_bytes_ctrl_c() {
598        let combo = parse_key_combo("Ctrl+C").unwrap();
599        let bytes = key_combo_to_bytes(&combo).unwrap();
600        assert_eq!(bytes, vec![0x03]); // Ctrl+C = ETX
601    }
602
603    #[test]
604    fn test_key_combo_to_bytes_ctrl_a() {
605        let combo = parse_key_combo("Ctrl+A").unwrap();
606        let bytes = key_combo_to_bytes(&combo).unwrap();
607        assert_eq!(bytes, vec![0x01]); // Ctrl+A = SOH
608    }
609
610    #[test]
611    fn test_key_combo_to_bytes_ctrl_z() {
612        let combo = parse_key_combo("Ctrl+Z").unwrap();
613        let bytes = key_combo_to_bytes(&combo).unwrap();
614        assert_eq!(bytes, vec![0x1a]); // Ctrl+Z = SUB
615    }
616
617    #[test]
618    fn test_key_combo_to_bytes_arrow_up() {
619        let combo = parse_key_combo("Up").unwrap();
620        let bytes = key_combo_to_bytes(&combo).unwrap();
621        assert_eq!(bytes, b"\x1b[A");
622    }
623
624    #[test]
625    fn test_key_combo_to_bytes_arrow_down() {
626        let combo = parse_key_combo("Down").unwrap();
627        let bytes = key_combo_to_bytes(&combo).unwrap();
628        assert_eq!(bytes, b"\x1b[B");
629    }
630
631    #[test]
632    fn test_key_combo_to_bytes_f5() {
633        let combo = parse_key_combo("F5").unwrap();
634        let bytes = key_combo_to_bytes(&combo).unwrap();
635        assert_eq!(bytes, b"\x1b[15~");
636    }
637
638    #[test]
639    fn test_key_combo_to_bytes_escape() {
640        let combo = parse_key_combo("Escape").unwrap();
641        let bytes = key_combo_to_bytes(&combo).unwrap();
642        assert_eq!(bytes, b"\x1b");
643    }
644
645    #[test]
646    fn test_key_combo_to_bytes_plain_char() {
647        let combo = parse_key_combo("A").unwrap();
648        let bytes = key_combo_to_bytes(&combo).unwrap();
649        assert_eq!(bytes, b"A");
650    }
651
652    #[test]
653    fn test_key_combo_to_bytes_alt_key() {
654        let combo = parse_key_combo("Alt+A").unwrap();
655        let bytes = key_combo_to_bytes(&combo).unwrap();
656        assert_eq!(bytes, vec![0x1b, b'A']);
657    }
658
659    #[test]
660    fn test_parse_key_sequence_single() {
661        let seqs = parse_key_sequence("Enter").unwrap();
662        assert_eq!(seqs.len(), 1);
663        assert_eq!(seqs[0], b"\r");
664    }
665
666    #[test]
667    fn test_parse_key_sequence_ctrl_c() {
668        let seqs = parse_key_sequence("Ctrl+C").unwrap();
669        assert_eq!(seqs.len(), 1);
670        assert_eq!(seqs[0], vec![0x03]);
671    }
672
673    #[test]
674    fn test_parse_key_sequence_multi_keys() {
675        let seqs = parse_key_sequence("Up Up Down Down").unwrap();
676        assert_eq!(seqs.len(), 4);
677        assert_eq!(seqs[0], b"\x1b[A");
678        assert_eq!(seqs[1], b"\x1b[A");
679        assert_eq!(seqs[2], b"\x1b[B");
680        assert_eq!(seqs[3], b"\x1b[B");
681    }
682
683    #[test]
684    fn test_parse_key_sequence_empty() {
685        assert!(parse_key_sequence("").is_err());
686        assert!(parse_key_sequence("   ").is_err());
687    }
688
689    #[test]
690    fn test_parse_key_sequence_invalid_key() {
691        assert!(parse_key_sequence("InvalidKey").is_err());
692    }
693
694    #[test]
695    fn test_key_combo_to_bytes_physical_key_error() {
696        let combo = parse_key_combo("Ctrl+[KeyZ]").unwrap();
697        assert!(key_combo_to_bytes(&combo).is_err());
698    }
699}