use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::fmt; use std::hash::{Hash, Hasher}; use crate::error::ParseError; #[derive(Clone, Copy, Debug)] pub struct Key { pub code: KeyCode, pub modifiers: KeyModifiers, } impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { self.code == other.code && self.modifiers == other.modifiers } } impl Eq for Key {} impl Hash for Key { fn hash(&self, state: &mut H) { hash_key_code(&self.code, state); self.modifiers.bits().hash(state); } } fn hash_key_code(code: &KeyCode, state: &mut H) { match code { KeyCode::Backspace => (0u8).hash(state), KeyCode::Enter => (1u8).hash(state), KeyCode::Left => (2u8).hash(state), KeyCode::Right => (3u8).hash(state), KeyCode::Up => (4u8).hash(state), KeyCode::Down => (5u8).hash(state), KeyCode::Home => (6u8).hash(state), KeyCode::End => (7u8).hash(state), KeyCode::PageUp => (8u8).hash(state), KeyCode::PageDown => (9u8).hash(state), KeyCode::Tab => (10u8).hash(state), KeyCode::BackTab => (11u8).hash(state), KeyCode::Delete => (12u8).hash(state), KeyCode::Insert => (13u8).hash(state), KeyCode::F(n) => { (14u8).hash(state); n.hash(state); } KeyCode::Char(c) => { (15u8).hash(state); c.hash(state); } KeyCode::Null => (16u8).hash(state), KeyCode::Esc => (17u8).hash(state), KeyCode::CapsLock => (18u8).hash(state), KeyCode::ScrollLock => (19u8).hash(state), KeyCode::NumLock => (20u8).hash(state), KeyCode::PrintScreen => (21u8).hash(state), KeyCode::Pause => (22u8).hash(state), KeyCode::Menu => (23u8).hash(state), KeyCode::KeypadBegin => (24u8).hash(state), KeyCode::Media(m) => { (25u8).hash(state); (*m as u8).hash(state); } KeyCode::Modifier(m) => { (26u8).hash(state); (*m as u8).hash(state); } } } impl From for Key { fn from(e: KeyEvent) -> Self { Key { code: e.code, modifiers: e.modifiers, } } } impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.modifiers.contains(KeyModifiers::CONTROL) { write!(f, "C-")?; } if self.modifiers.contains(KeyModifiers::SHIFT) { write!(f, "S-")?; } if self.modifiers.contains(KeyModifiers::ALT) { write!(f, "A-")?; } match self.code { KeyCode::Char(' ') => write!(f, "SPC"), KeyCode::Char(c) => write!(f, "{c}"), KeyCode::Esc => write!(f, "Esc"), KeyCode::Tab => write!(f, "Tab"), KeyCode::Enter => write!(f, "Enter"), KeyCode::Backspace => write!(f, "Backspace"), KeyCode::F(n) => write!(f, "F{n}"), KeyCode::Left => write!(f, "Left"), KeyCode::Right => write!(f, "Right"), KeyCode::Up => write!(f, "Up"), KeyCode::Down => write!(f, "Down"), KeyCode::Home => write!(f, "Home"), KeyCode::End => write!(f, "End"), KeyCode::PageUp => write!(f, "PageUp"), KeyCode::PageDown => write!(f, "PageDown"), KeyCode::Delete => write!(f, "Delete"), KeyCode::Insert => write!(f, "Insert"), _ => write!(f, "?"), } } } pub fn parse_key(input: &str) -> Result { if input.trim().is_empty() { return Err(ParseError::EmptyInput); } if input.contains(' ') { return Err(ParseError::UnknownKey(input.to_string())); } let trimmed = input; let mut modifiers = KeyModifiers::NONE; let mut remaining = trimmed; let mut has_ctrl = false; let mut has_shift = false; let mut has_alt = false; loop { if remaining.starts_with("C-") { if has_ctrl { return Err(ParseError::DuplicateModifier); } has_ctrl = true; modifiers |= KeyModifiers::CONTROL; remaining = &remaining[2..]; } else if remaining.starts_with("S-") { if has_shift { return Err(ParseError::DuplicateModifier); } has_shift = true; modifiers |= KeyModifiers::SHIFT; remaining = &remaining[2..]; } else if remaining.starts_with("A-") { if has_alt { return Err(ParseError::DuplicateModifier); } has_alt = true; modifiers |= KeyModifiers::ALT; remaining = &remaining[2..]; } else { break; } } if remaining.is_empty() { return Err(ParseError::DanglingModifier); } let code = match remaining { "SPC" => KeyCode::Char(' '), "Esc" => KeyCode::Esc, "Tab" => KeyCode::Tab, "Enter" => KeyCode::Enter, "Backspace" => KeyCode::Backspace, "Left" => KeyCode::Left, "Right" => KeyCode::Right, "Up" => KeyCode::Up, "Down" => KeyCode::Down, "Home" => KeyCode::Home, "End" => KeyCode::End, "PageUp" => KeyCode::PageUp, "PageDown" => KeyCode::PageDown, "Delete" => KeyCode::Delete, "Insert" => KeyCode::Insert, "-" => KeyCode::Char('-'), s if s.starts_with('F') && s.len() > 1 => { let num_str = &s[1..]; match num_str.parse::() { Ok(n) if (1..=12).contains(&n) => KeyCode::F(n), _ => return Err(ParseError::UnknownKey(input.to_string())), } } s if s.len() == 1 => { let c = s.chars().next().expect("non-empty string"); if !c.is_ascii() { return Err(ParseError::UnknownKey(input.to_string())); } if has_shift && c.is_ascii_uppercase() { return Err(ParseError::RedundantShift); } KeyCode::Char(c) } _ => return Err(ParseError::UnknownKey(input.to_string())), }; Ok(Key { code, modifiers }) } pub fn parse_sequence(input: &str) -> Result, ParseError> { let trimmed = input.trim(); if trimmed.is_empty() { return Err(ParseError::EmptyInput); } trimmed.split_whitespace().map(parse_key).collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_empty_input() { assert_eq!(parse_key(""), Err(ParseError::EmptyInput)); assert_eq!(parse_key(" "), Err(ParseError::EmptyInput)); } #[test] fn test_space_in_key() { assert!(matches!(parse_key(" j"), Err(ParseError::UnknownKey(_)))); } #[test] fn test_dangling_modifier() { assert_eq!(parse_key("C-"), Err(ParseError::DanglingModifier)); } #[test] fn test_duplicate_modifier() { assert_eq!(parse_key("C-C-d"), Err(ParseError::DuplicateModifier)); } #[test] fn test_lowercase_modifier() { assert!(matches!(parse_key("c-d"), Err(ParseError::UnknownKey(_)))); } #[test] fn test_ctrl_d() { let key = parse_key("C-d").unwrap(); assert_eq!(key.code, KeyCode::Char('d')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); } #[test] fn test_ctrl_shift_d() { let key = parse_key("C-S-d").unwrap(); assert_eq!(key.code, KeyCode::Char('d')); assert_eq!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SHIFT); } #[test] fn test_modifier_order_normalized() { let key1 = parse_key("S-C-d").unwrap(); let key2 = parse_key("C-S-d").unwrap(); assert_eq!(key1, key2); } #[test] fn test_uppercase_no_shift() { let key = parse_key("G").unwrap(); assert_eq!(key.code, KeyCode::Char('G')); assert_eq!(key.modifiers, KeyModifiers::NONE); } #[test] fn test_shift_lowercase() { let key = parse_key("S-g").unwrap(); assert_eq!(key.code, KeyCode::Char('g')); assert_eq!(key.modifiers, KeyModifiers::SHIFT); } #[test] fn test_redundant_shift() { assert_eq!(parse_key("S-G"), Err(ParseError::RedundantShift)); } #[test] fn test_bare_modifier_letters() { let key = parse_key("C").unwrap(); assert_eq!(key.code, KeyCode::Char('C')); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("S").unwrap(); assert_eq!(key.code, KeyCode::Char('S')); assert_eq!(key.modifiers, KeyModifiers::NONE); } #[test] fn test_special_keys() { let key = parse_key("SPC").unwrap(); assert_eq!(key.code, KeyCode::Char(' ')); assert_eq!(key.modifiers, KeyModifiers::NONE); assert!(matches!(parse_key("spc"), Err(ParseError::UnknownKey(_)))); let key = parse_key("Esc").unwrap(); assert_eq!(key.code, KeyCode::Esc); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("Tab").unwrap(); assert_eq!(key.code, KeyCode::Tab); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("Enter").unwrap(); assert_eq!(key.code, KeyCode::Enter); assert_eq!(key.modifiers, KeyModifiers::NONE); // Tab != 't' assert_ne!(parse_key("Tab").unwrap(), parse_key("t").unwrap()); } #[test] fn test_function_keys() { let key = parse_key("F1").unwrap(); assert_eq!(key.code, KeyCode::F(1)); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("F12").unwrap(); assert_eq!(key.code, KeyCode::F(12)); assert_eq!(key.modifiers, KeyModifiers::NONE); assert!(matches!(parse_key("F0"), Err(ParseError::UnknownKey(_)))); assert!(matches!(parse_key("F13"), Err(ParseError::UnknownKey(_)))); let key = parse_key("F").unwrap(); assert_eq!(key.code, KeyCode::Char('F')); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("C-F1").unwrap(); assert_eq!(key.code, KeyCode::F(1)); assert_eq!(key.modifiers, KeyModifiers::CONTROL); } #[test] fn test_symbols() { let key = parse_key("-").unwrap(); assert_eq!(key.code, KeyCode::Char('-')); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("C--").unwrap(); assert_eq!(key.code, KeyCode::Char('-')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); let key = parse_key("[").unwrap(); assert_eq!(key.code, KeyCode::Char('[')); assert_eq!(key.modifiers, KeyModifiers::NONE); let key = parse_key("?").unwrap(); assert_eq!(key.code, KeyCode::Char('?')); assert_eq!(key.modifiers, KeyModifiers::NONE); } #[test] fn test_digits() { for c in '0'..='9' { let key = parse_key(&c.to_string()).unwrap(); assert_eq!(key.code, KeyCode::Char(c)); assert_eq!(key.modifiers, KeyModifiers::NONE); } } #[test] fn test_invalid_non_ascii() { assert!(matches!(parse_key("é"), Err(ParseError::UnknownKey(_)))); } #[test] fn test_space_is_sequence() { assert!(matches!(parse_key("g g"), Err(ParseError::UnknownKey(_)))); } #[test] fn test_display_roundtrip() { let test_cases = [ "j", "G", "C-d", "C-S-d", "SPC", "Esc", "Tab", "Enter", "F1", "-", "C--", "[", ]; for input in test_cases { let key = parse_key(input).unwrap(); let displayed = key.to_string(); let reparsed = parse_key(&displayed).unwrap(); assert_eq!(key, reparsed, "roundtrip failed for {input}"); } } #[test] fn test_display_specific() { let key = Key { code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, }; assert_eq!(key.to_string(), "SPC"); let key = Key { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT, }; assert_eq!(key.to_string(), "C-S-d"); } #[test] fn test_sequence_parsing() { let seq = parse_sequence("g g").unwrap(); assert_eq!(seq.len(), 2); assert_eq!(seq[0].code, KeyCode::Char('g')); assert_eq!(seq[1].code, KeyCode::Char('g')); let seq = parse_sequence("SPC b l").unwrap(); assert_eq!(seq.len(), 3); assert_eq!(seq[0].code, KeyCode::Char(' ')); assert_eq!(seq[1].code, KeyCode::Char('b')); assert_eq!(seq[2].code, KeyCode::Char('l')); assert_eq!(parse_sequence(""), Err(ParseError::EmptyInput)); assert_eq!(parse_sequence(" "), Err(ParseError::EmptyInput)); } #[test] fn test_from_key_event() { let event = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE); let key = Key::from(event); assert_eq!(key.code, KeyCode::Char('j')); assert_eq!(key.modifiers, KeyModifiers::NONE); } #[test] fn test_key_hash() { use std::collections::HashMap; let mut map = HashMap::new(); let key1 = parse_key("C-d").unwrap(); let key2 = parse_key("C-d").unwrap(); map.insert(key1, "action"); assert_eq!(map.get(&key2), Some(&"action")); } }