// client/src/config/key_sequences.rs use crossterm::event::{KeyCode, KeyModifiers}; use std::time::{Duration, Instant}; use tracing::info; #[derive(Debug, Clone, PartialEq)] pub struct ParsedKey { pub code: KeyCode, pub modifiers: KeyModifiers, } #[derive(Debug, Clone)] pub struct KeySequenceTracker { pub current_sequence: Vec, pub last_key_time: Instant, pub timeout: Duration, } impl KeySequenceTracker { pub fn new(timeout_ms: u64) -> Self { Self { current_sequence: Vec::new(), last_key_time: Instant::now(), timeout: Duration::from_millis(timeout_ms), } } pub fn reset(&mut self) { info!("KeySequenceTracker.reset() from {:?}", self.current_sequence); self.current_sequence.clear(); self.last_key_time = Instant::now(); } pub fn add_key(&mut self, key: KeyCode) -> bool { let now = Instant::now(); if now.duration_since(self.last_key_time) > self.timeout { info!("KeySequenceTracker timeout — reset before adding {:?}", key); self.reset(); } self.current_sequence.push(key); self.last_key_time = now; info!("KeySequenceTracker state after add: {:?}", self.current_sequence); true } pub fn get_sequence(&self) -> Vec { self.current_sequence.clone() } // Convert a sequence of keys to a string representation pub fn sequence_to_string(&self) -> String { self.current_sequence.iter().map(|k| key_to_string(k)).collect() } // Convert a sequence to a format with + between keys pub fn sequence_to_plus_format(&self) -> String { if self.current_sequence.is_empty() { return String::new(); } let parts: Vec = self.current_sequence.iter() .map(|k| key_to_string(k)) .collect(); parts.join("+") } } // Helper function to convert any KeyCode to a string representation pub fn key_to_string(key: &KeyCode) -> String { match key { KeyCode::Char(' ') => "space".to_string(), KeyCode::Char(c) => c.to_string(), KeyCode::Left => "left".to_string(), KeyCode::Right => "right".to_string(), KeyCode::Up => "up".to_string(), KeyCode::Down => "down".to_string(), KeyCode::Esc => "esc".to_string(), KeyCode::Enter => "enter".to_string(), KeyCode::Backspace => "backspace".to_string(), KeyCode::Delete => "delete".to_string(), KeyCode::Tab => "tab".to_string(), KeyCode::BackTab => "backtab".to_string(), KeyCode::Home => "home".to_string(), KeyCode::End => "end".to_string(), KeyCode::PageUp => "pageup".to_string(), KeyCode::PageDown => "pagedown".to_string(), KeyCode::Insert => "insert".to_string(), _ => format!("{:?}", key).to_lowercase(), } } // Helper function to convert a string to a KeyCode pub fn string_to_keycode(s: &str) -> Option { match s.to_lowercase().as_str() { "space" => Some(KeyCode::Char(' ')), "left" => Some(KeyCode::Left), "right" => Some(KeyCode::Right), "up" => Some(KeyCode::Up), "down" => Some(KeyCode::Down), "esc" => Some(KeyCode::Esc), "enter" => Some(KeyCode::Enter), "backspace" => Some(KeyCode::Backspace), "delete" => Some(KeyCode::Delete), "tab" => Some(KeyCode::Tab), "backtab" => Some(KeyCode::BackTab), "home" => Some(KeyCode::Home), "end" => Some(KeyCode::End), "pageup" => Some(KeyCode::PageUp), "pagedown" => Some(KeyCode::PageDown), "insert" => Some(KeyCode::Insert), s if s.len() == 1 => s.chars().next().map(KeyCode::Char), _ => None, } } pub fn parse_binding(binding: &str) -> Vec { let mut sequence = Vec::new(); // Split into multi-key sequence: // - If contains space → sequence split by space // - Else split by '+' let parts: Vec<&str> = if binding.contains(' ') { binding.split(' ').collect() } else { binding.split('+').collect() }; for part in parts { if let Some(parsed) = parse_key_part(part) { sequence.push(parsed); } } sequence } fn is_compound_key(part: &str) -> bool { matches!(part.to_lowercase().as_str(), "esc" | "up" | "down" | "left" | "right" | "enter" | "backspace" | "delete" | "tab" | "backtab" | "home" | "end" | "pageup" | "pagedown" | "insert" | "space" ) } fn parse_key_part(part: &str) -> Option { let mut modifiers = KeyModifiers::empty(); let mut code = None; if part.contains('+') { // This handles modifiers like "ctrl+s", "super+shift+f5" let components: Vec<&str> = part.split('+').collect(); for component in components { match component.to_lowercase().as_str() { "ctrl" | "control" => modifiers |= KeyModifiers::CONTROL, "shift" => modifiers |= KeyModifiers::SHIFT, "alt" => modifiers |= KeyModifiers::ALT, "super" | "windows" | "cmd" => modifiers |= KeyModifiers::SUPER, "hyper" => modifiers |= KeyModifiers::HYPER, "meta" => modifiers |= KeyModifiers::META, _ => { // Last component is the key code = string_to_keycode(component); } } } } else { // Simple key without modifiers code = string_to_keycode(part); } code.map(|code| ParsedKey { code, modifiers }) }