diff --git a/client/config.toml b/client/config.toml index 1319cb1..470cddf 100644 --- a/client/config.toml +++ b/client/config.toml @@ -7,8 +7,8 @@ save_and_quit = [":wq", "ctrl+shift+s"] enter_edit_mode_before = ["i"] enter_edit_mode_after = ["a"] exit_edit_mode = ["esc", "ctrl+e"] -previous_entry = ["Left", "q"] # Changed from previous_position -next_entry = ["Right", "1"] # Changed from next_position +previous_entry = ["q"] # Changed from previous_position +next_entry = ["right","1"] # Changed from next_position move_left = ["h"] move_right = ["l"] @@ -21,7 +21,7 @@ move_word_end_prev = ["ge"] # Move to end of previous word move_line_start = ["0"] # Move to beginning of line move_line_end = ["$"] # Move to end of line move_first_line = ["gg"] # Move to first line of form -move_last_line = ["g+enter"] +move_last_line = ["ctrl+g"] [colors] theme = "dark" diff --git a/client/src/config/config.rs b/client/src/config/config.rs index d596dcd..e118708 100644 --- a/client/src/config/config.rs +++ b/client/src/config/config.rs @@ -186,4 +186,102 @@ impl Config { } false } + + // This method handles all keybinding formats, both with and without + + pub fn matches_key_sequence_generalized(&self, sequence: &[KeyCode]) -> Option<&str> { + if sequence.is_empty() { + return None; + } + + // Get string representations of the sequence + let sequence_str = sequence.iter() + .map(|k| crate::config::key_sequences::key_to_string(k)) + .collect::>() + .join(""); + + // Add the missing sequence_plus definition + let sequence_plus = sequence.iter() + .map(|k| crate::config::key_sequences::key_to_string(k)) + .collect::>() + .join("+"); + + // Check for matches in all binding formats + for (action, bindings) in &self.keybindings { + for binding in bindings { + let normalized_binding = binding.to_lowercase(); + + // Check if binding matches any of our formats + if normalized_binding == sequence_str || normalized_binding == sequence_plus { + return Some(action); + } + + // Special case for + format in bindings + if binding.contains('+') { + let normalized_sequence = sequence.iter() + .map(|k| crate::config::key_sequences::key_to_string(k)) + .collect::>(); + + let binding_parts: Vec<&str> = binding.split('+').collect(); + + if binding_parts.len() == sequence.len() { + let matches = binding_parts.iter().enumerate().all(|(i, part)| { + part.to_lowercase() == normalized_sequence[i].to_lowercase() + }); + + if matches { + return Some(action); + } + } + } + } + } + + None + } + + // Check if the current key sequence is a prefix of a longer binding + pub fn is_key_sequence_prefix(&self, sequence: &[KeyCode]) -> bool { + if sequence.is_empty() { + return false; + } + + // Get string representation of the sequence + let sequence_str = sequence.iter() + .map(|k| crate::config::key_sequences::key_to_string(k)) + .collect::>() + .join(""); + + // Check all bindings to see if our sequence is a prefix + for (_, bindings) in &self.keybindings { + for binding in bindings { + let normalized_binding = binding.to_lowercase(); + + // Check standard format + if normalized_binding.starts_with(&sequence_str) && + normalized_binding.len() > sequence_str.len() { + return true; + } + + // Check + format + if binding.contains('+') { + let binding_parts: Vec<&str> = binding.split('+').collect(); + let sequence_parts = sequence.iter() + .map(|k| crate::config::key_sequences::key_to_string(k)) + .collect::>(); + + if binding_parts.len() > sequence_parts.len() { + let prefix_matches = sequence_parts.iter().enumerate().all(|(i, part)| { + binding_parts.get(i).map_or(false, |b| b.to_lowercase() == part.to_lowercase()) + }); + + if prefix_matches { + return true; + } + } + } + } + } + + false + } } diff --git a/client/src/config/key_sequences.rs b/client/src/config/key_sequences.rs index 7e5567b..fc371f0 100644 --- a/client/src/config/key_sequences.rs +++ b/client/src/config/key_sequences.rs @@ -1,4 +1,4 @@ -// client/src/key_sequences.rs +// client/src/config/key_sequences.rs use crossterm::event::{KeyCode, KeyModifiers}; use std::time::{Duration, Instant}; @@ -35,7 +35,7 @@ impl KeySequenceTracker { if now.duration_since(self.last_key_time) > self.timeout { self.reset(); } - + self.current_sequence.push(key); self.last_key_time = now; true @@ -44,38 +44,91 @@ impl KeySequenceTracker { 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| match k { - KeyCode::Char(c) => c.to_string(), - KeyCode::Left => "Left".into(), - KeyCode::Right => "Right".into(), - KeyCode::Up => "Up".into(), - KeyCode::Down => "Down".into(), - KeyCode::Esc => "Esc".into(), - KeyCode::Enter => "Enter".into(), - _ => String::new(), - }).collect() + 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(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() { + "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(); - let parts: Vec<&str> = if binding.contains(' ') { - binding.split(' ').collect() - } else if is_special_key(binding) { - vec![binding] + // Handle different binding formats + let parts: Vec = if binding.contains('+') { + // Format with explicit '+' separators like "g+left" + binding.split('+').map(|s| s.to_string()).collect() + } else if binding.contains(' ') { + // Format with spaces like "g left" + binding.split(' ').map(|s| s.to_string()).collect() + } else if is_compound_key(binding) { + // A single compound key like "left" or "enter" + vec![binding.to_string()] } else { - // Fixed implementation using char indices - binding.char_indices().map(|(i, _)| { - let start = i; - let end = i + 1; - &binding[start..end] - }).collect() + // Simple character sequence like "gg" + binding.chars().map(|c| c.to_string()).collect() }; - for part in parts { + for part in &parts { if let Some(key) = parse_key_part(part) { sequence.push(key); } @@ -83,39 +136,36 @@ pub fn parse_binding(binding: &str) -> Vec { sequence } -fn is_special_key(part: &str) -> bool { - matches!(part.to_lowercase().as_str(), - "esc" | "up" | "down" | "left" | "right" | - "enter" | "backspace" | "delete" | "tab" | "backtab" +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" ) } fn parse_key_part(part: &str) -> Option { let mut modifiers = KeyModifiers::empty(); let mut code = None; - let components: Vec<&str> = part.split('+').collect(); - for component in components { - match component.to_lowercase().as_str() { - "ctrl" => modifiers |= KeyModifiers::CONTROL, - "shift" => modifiers |= KeyModifiers::SHIFT, - "alt" => modifiers |= KeyModifiers::ALT, - "esc" => code = Some(KeyCode::Esc), - "left" => code = Some(KeyCode::Left), - "right" => code = Some(KeyCode::Right), - "up" => code = Some(KeyCode::Up), - "down" => code = Some(KeyCode::Down), - "enter" => code = Some(KeyCode::Enter), - "backspace" => code = Some(KeyCode::Backspace), - "delete" => code = Some(KeyCode::Delete), - "tab" => code = Some(KeyCode::Tab), - "backtab" => code = Some(KeyCode::BackTab), - ":" => code = Some(KeyCode::Char(':')), - c if c.len() == 1 => { - code = Some(KeyCode::Char(c.chars().next().unwrap())); + if part.contains('+') { + // This handles modifiers like "ctrl+s" + let components: Vec<&str> = part.split('+').collect(); + + for component in components { + match component.to_lowercase().as_str() { + "ctrl" => modifiers |= KeyModifiers::CONTROL, + "shift" => modifiers |= KeyModifiers::SHIFT, + "alt" => modifiers |= KeyModifiers::ALT, + _ => { + // Last component is the key + code = string_to_keycode(component); + } } - _ => return None, } + } else { + // Simple key without modifiers + code = string_to_keycode(part); } code.map(|code| ParsedKey { code, modifiers }) diff --git a/client/src/modes/handlers/read_only.rs b/client/src/modes/handlers/read_only.rs index c1c884e..37e16c0 100644 --- a/client/src/modes/handlers/read_only.rs +++ b/client/src/modes/handlers/read_only.rs @@ -1,6 +1,6 @@ // src/modes/handlers/read_only.rs -use crossterm::event::{KeyEvent, KeyCode}; +use crossterm::event::{KeyEvent}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use crate::config::key_sequences::KeySequenceTracker; @@ -13,6 +13,7 @@ enum CharType { Punctuation, } +// Replace your current handle_read_only_event with this generalized version pub async fn handle_read_only_event( key: KeyEvent, config: &Config, @@ -25,14 +26,38 @@ pub async fn handle_read_only_event( edit_mode_cooldown: &mut bool, ideal_cursor_column: &mut usize, ) -> Result<(bool, String), Box> { - // Handle key sequences and actions - if let KeyCode::Char(_) = key.code { - if key.modifiers.is_empty() { - key_sequence_tracker.add_key(key.code); - let sequence = key_sequence_tracker.get_sequence(); - let sequence_str = key_sequence_tracker.sequence_to_string(); + // Always add the key to the sequence tracker if no modifiers + // This tracks ALL keys, not just character keys + if key.modifiers.is_empty() { + key_sequence_tracker.add_key(key.code); + let sequence = key_sequence_tracker.get_sequence(); - if let Some(action) = config.matches_key_sequence(&sequence) { + // Try to match the current sequence against all bindings + if let Some(action) = config.matches_key_sequence_generalized(&sequence) { + let result = execute_action( + action, + form_state, + ideal_cursor_column, + key_sequence_tracker, + command_message, + current_position, + total_count, + app_terminal, + ).await?; + key_sequence_tracker.reset(); + return Ok((false, result)); + } + + // Check if this might be a prefix of a longer sequence + if config.is_key_sequence_prefix(&sequence) { + // If it's a prefix, wait for more keys + return Ok((false, command_message.clone())); + } + + // Since it's not part of a multi-key sequence, check for a direct action + if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) { + // Try to handle it as a single key + if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { let result = execute_action( action, form_state, @@ -46,33 +71,11 @@ pub async fn handle_read_only_event( key_sequence_tracker.reset(); return Ok((false, result)); } - - if sequence.len() == 1 { - let is_prefix = is_prefix_of_multikey_binding(&sequence_str, config); - if !is_prefix { - if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { - let result = execute_action( - action, - form_state, - ideal_cursor_column, - key_sequence_tracker, - command_message, - current_position, - total_count, - app_terminal, - ).await?; - key_sequence_tracker.reset(); - return Ok((false, result)); - } - } - } - } else { - key_sequence_tracker.reset(); } } else { + // If modifiers are pressed, check for direct key bindings key_sequence_tracker.reset(); - // Handle special keys through config if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { let result = execute_action( action, @@ -86,15 +89,15 @@ pub async fn handle_read_only_event( ).await?; return Ok((false, result)); } + } - // Provide feedback when not in edit mode and cooldown expired - if !*edit_mode_cooldown { - let default_key = "i".to_string(); - let edit_key = config.keybindings.get("enter_edit_mode") - .and_then(|keys| keys.first()) - .unwrap_or(&default_key); - *command_message = format!("Read-only mode - press {} to edit", edit_key); - } + // Show a helpful message when no binding was found + if !*edit_mode_cooldown { + let default_key = "i".to_string(); + let edit_key = config.keybindings.get("enter_edit_mode_before") + .and_then(|keys| keys.first()) + .unwrap_or(&default_key); + *command_message = format!("Read-only mode - press {} to edit", edit_key); } Ok((false, command_message.clone())) @@ -184,7 +187,6 @@ async fn execute_action( Ok("".to_string()) } "move_left" => { - let current_input = form_state.get_current_input(); let current_pos = form_state.current_cursor_pos; form_state.current_cursor_pos = current_pos.saturating_sub(1); *ideal_cursor_column = form_state.current_cursor_pos;