From a7c105d903132d017273bc8c2b3cb056e04c40be Mon Sep 17 00:00:00 2001 From: filipriec Date: Thu, 27 Feb 2025 13:58:47 +0100 Subject: [PATCH] working double shortcuts --- client/config.toml | 2 +- client/src/config/config.rs | 46 ++++ client/src/config/key_sequences.rs | 115 ++++++++++ client/src/config/mod.rs | 1 + client/src/modes/handlers/event.rs | 335 ++++++++++++++++------------- 5 files changed, 345 insertions(+), 154 deletions(-) create mode 100644 client/src/config/key_sequences.rs diff --git a/client/config.toml b/client/config.toml index cd3176c..b4bb591 100644 --- a/client/config.toml +++ b/client/config.toml @@ -20,7 +20,7 @@ move_word_prev = ["b"] # Move to beginning of previous word 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 = ["t"] # Move to first line of form +move_first_line = ["gg"] # Move to first line of form move_last_line = ["G"] # Move to last line of form [colors] diff --git a/client/src/config/config.rs b/client/src/config/config.rs index 8e1d1ed..3be0647 100644 --- a/client/src/config/config.rs +++ b/client/src/config/config.rs @@ -46,6 +46,11 @@ impl Config { continue; } for binding in bindings { + // Skip multi-key bindings when checking for single key actions + if binding.len() > 1 && !binding.contains('+') { + continue; + } + if Self::matches_keybinding(binding, key, modifiers) { return Some(action); } @@ -54,11 +59,52 @@ impl Config { None } + /// Checks if a sequence of keys matches any keybinding + pub fn matches_key_sequence(&self, sequence: &[KeyCode]) -> Option<&str> { + if sequence.is_empty() { + return None; + } + + // Convert key sequence to a string (for simple character sequences) + let sequence_str: String = sequence.iter().filter_map(|key| { + if let KeyCode::Char(c) = key { + Some(*c) + } else { + None + } + }).collect(); + + if sequence_str.is_empty() { + return None; + } + + // Check if this sequence matches any binding + for (action, bindings) in &self.keybindings { + for binding in bindings { + // Skip bindings with modifiers (those contain '+') + if binding.contains('+') { + continue; + } + + if binding == &sequence_str { + return Some(action); + } + } + } + + None + } + fn matches_keybinding( binding: &str, key: KeyCode, modifiers: KeyModifiers, ) -> bool { + // For multi-character bindings without modifiers, we handle them in matches_key_sequence + if binding.len() > 1 && !binding.contains('+') { + return false; + } + let parts: Vec<&str> = binding.split('+').collect(); let mut expected_modifiers = KeyModifiers::empty(); let mut expected_key = None; diff --git a/client/src/config/key_sequences.rs b/client/src/config/key_sequences.rs new file mode 100644 index 0000000..b0bea5e --- /dev/null +++ b/client/src/config/key_sequences.rs @@ -0,0 +1,115 @@ +use crossterm::event::{KeyCode, KeyModifiers}; +use std::time::{Duration, Instant}; + +#[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) { + self.current_sequence.clear(); + self.last_key_time = Instant::now(); + } + + pub fn add_key(&mut self, key: KeyCode) -> bool { + // Check if timeout has expired + let now = Instant::now(); + if now.duration_since(self.last_key_time) > self.timeout { + self.reset(); + } + + self.current_sequence.push(key); + self.last_key_time = now; + true + } + + pub fn get_sequence(&self) -> Vec { + self.current_sequence.clone() + } + + pub fn sequence_to_string(&self) -> String { + self.current_sequence.iter().map(|k| match k { + KeyCode::Char(c) => c.to_string(), + _ => String::new(), + }).collect() + } +} + +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] + } else { + // Fixed implementation using char indices + binding.char_indices().map(|(i, _)| { + let start = i; + let end = i + 1; + &binding[start..end] + }).collect() + }; + + for part in parts { + if let Some(key) = parse_key_part(part) { + sequence.push(key); + } + } + 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 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), + "up" => code = Some(KeyCode::Up), + "down" => code = Some(KeyCode::Down), + "left" => code = Some(KeyCode::Left), + "right" => code = Some(KeyCode::Right), + "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())); + } + _ => return None, + } + } + + code.map(|code| ParsedKey { code, modifiers }) +} diff --git a/client/src/config/mod.rs b/client/src/config/mod.rs index ee2bbb3..c7fe975 100644 --- a/client/src/config/mod.rs +++ b/client/src/config/mod.rs @@ -1,3 +1,4 @@ // src/config/mod.rs pub mod colors; pub mod config; +pub mod key_sequences; diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 05a834a..d89ba58 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -1,11 +1,12 @@ // src/modes/handlers/event.rs -use crossterm::event::{Event, KeyCode, KeyModifiers}; +use crossterm::event::{Event, KeyCode}; use crossterm::cursor::{SetCursorStyle}; use crate::tui::terminal::AppTerminal; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use crate::modes::handlers::edit::handle_edit_event; +use crate::config::key_sequences::KeySequenceTracker; use crate::modes::handlers::command_mode::handle_command_event; pub struct EventHandler { @@ -15,6 +16,7 @@ pub struct EventHandler { pub is_edit_mode: bool, pub edit_mode_cooldown: bool, pub ideal_cursor_column: usize, + pub key_sequence_tracker: KeySequenceTracker, } #[derive(PartialEq)] @@ -33,10 +35,10 @@ impl EventHandler { is_edit_mode: false, edit_mode_cooldown: false, ideal_cursor_column: 0, + key_sequence_tracker: KeySequenceTracker::new(800), } } - pub async fn handle_event( &mut self, event: Event, @@ -61,33 +63,29 @@ impl EventHandler { current_position, total_count, ).await?; - + if exit_command_mode { self.command_mode = false; } - + if !message.is_empty() { return Ok((should_exit, message)); } - - // If we're still in command mode, don't process other keys + if self.command_mode { return Ok((false, "".to_string())); } - } - // Only trigger command mode with ":" in read-only mode, not in edit mode + } else if !self.is_edit_mode && key.code == KeyCode::Char(':') { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); return Ok((false, "".to_string())); } - + // Handle mode transitions if !self.is_edit_mode && config.is_enter_edit_mode(key.code, key.modifiers) { - // Determine which type of edit mode we're entering if config.is_enter_edit_mode_after(key.code, key.modifiers) { - // For 'a' (append) mode: Move cursor position one character to the right let current_input = form_state.get_current_input(); if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() { form_state.current_cursor_pos += 1; @@ -119,8 +117,7 @@ impl EventHandler { } if self.is_edit_mode { - // Delegate edit mode event handling to edit.rs - return handle_edit_event( + let result = handle_edit_event( key, config, form_state, @@ -135,6 +132,8 @@ impl EventHandler { current_position, total_count, ).await; + self.key_sequence_tracker.reset(); + return result; } else { // Handle navigation between entries if key.code == KeyCode::Left { @@ -143,10 +142,7 @@ impl EventHandler { *current_position = new_position; match app_terminal.get_adresar_by_position(*current_position).await { Ok(response) => { - // Update the ID field - this is what was missing form_state.id = response.id; - - // Update all form fields dynamically form_state.values = vec![ response.firma, response.kz, @@ -167,7 +163,7 @@ impl EventHandler { let current_input = form_state.get_current_input(); let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { - current_input.len() - 1 // In readonly mode, limit to last character + current_input.len() - 1 } else { current_input.len() }; @@ -179,6 +175,7 @@ impl EventHandler { self.command_message = format!("Error loading entry: {}", e); } } + self.key_sequence_tracker.reset(); return Ok((false, self.command_message.clone())); } } else if key.code == KeyCode::Right { @@ -188,11 +185,7 @@ impl EventHandler { if *current_position <= total_count { match app_terminal.get_adresar_by_position(*current_position).await { Ok(response) => { - - // Update the ID field - this was missing form_state.id = response.id; - - // Update all form fields dynamically form_state.values = vec![ response.firma, response.kz, @@ -213,7 +206,7 @@ impl EventHandler { let current_input = form_state.get_current_input(); let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { - current_input.len() - 1 // In readonly mode, limit to last character + current_input.len() - 1 } else { current_input.len() }; @@ -226,149 +219,54 @@ impl EventHandler { } } } else { - // Clear form when entering new entry position form_state.reset_to_empty(); form_state.current_field = 0; form_state.current_cursor_pos = 0; self.command_message = "New entry mode".to_string(); } + self.key_sequence_tracker.reset(); return Ok((false, self.command_message.clone())); } } else { - // Handle movement keybindings - if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { - match action { - "move_left" => { - form_state.current_cursor_pos = form_state.current_cursor_pos.saturating_sub(1); - self.ideal_cursor_column = form_state.current_cursor_pos; - return Ok((false, "".to_string())); + // Handle key sequences and single key actions + if let KeyCode::Char(_) = key.code { + if key.modifiers.is_empty() { + self.key_sequence_tracker.add_key(key.code); + let sequence = self.key_sequence_tracker.get_sequence(); + + // First check for multi-key sequences + if let Some(action) = config.matches_key_sequence(&sequence) { + let result = self.execute_action(action, form_state)?; + self.key_sequence_tracker.reset(); + return Ok((false, result)); } - "move_right" => { - let current_input = form_state.get_current_input(); - // Only move right if there are characters and we're not at the last one - if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() - 1 { - form_state.current_cursor_pos += 1; + + // If single key, check for single key actions + if sequence.len() == 1 { + if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { + let result = self.execute_action(action, form_state)?; + return Ok((false, result)); } - self.ideal_cursor_column = form_state.current_cursor_pos; - return Ok((false, "".to_string())); - }, - "move_up" => { - if form_state.current_field == 0 { - // Wrap to the last field when at the top - form_state.current_field = form_state.fields.len() - 1; - } else { - form_state.current_field = form_state.current_field.saturating_sub(1); - } - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 // In readonly mode, limit to last character - } else { - 0 - }; - form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); - return Ok((false, "".to_string())); } - "move_down" => { - form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 // In readonly mode, limit to last character - } else { - 0 - }; - form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); - return Ok((false, "".to_string())); - } - "move_word_next" => { - let current_input = form_state.get_current_input(); - if !current_input.is_empty() { - let new_pos = self.find_next_word_start(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); - self.ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - "move_word_end" => { - let current_input = form_state.get_current_input(); - if !current_input.is_empty() { - let new_pos = self.find_word_end(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); - self.ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - "move_word_prev" => { - let current_input = form_state.get_current_input(); - if !current_input.is_empty() { - let new_pos = self.find_prev_word_start(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos; - self.ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - "move_word_end_prev" => { - let current_input = form_state.get_current_input(); - if !current_input.is_empty() { - let new_pos = self.find_prev_word_end(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos; - self.ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - "move_line_start" => { - form_state.current_cursor_pos = 0; - self.ideal_cursor_column = form_state.current_cursor_pos; - return Ok((false, "".to_string())); - } - "move_line_end" => { - let current_input = form_state.get_current_input(); - if !current_input.is_empty() { - form_state.current_cursor_pos = current_input.len() - 1; - self.ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - "move_first_line" => { - // Jump to first field - form_state.current_field = 0; - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { - current_input.len() - 1 - } else { - current_input.len() - }; - form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); - return Ok((false, "".to_string())); - } - "move_last_line" => { - // Jump to last field - form_state.current_field = form_state.fields.len() - 1; - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { - current_input.len() - 1 - } else { - current_input.len() - }; - form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); - return Ok((false, "".to_string())); - } - _ => {} + } else { + self.key_sequence_tracker.reset(); } - } - // Handle other keys in read-only mode - match key.code { - KeyCode::Esc => { - self.command_mode = false; - self.command_input.clear(); - self.command_message.clear(); - } - _ => { - if !self.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); - self.command_message = format!("Read-only mode - press {} to edit", edit_key); + } else { + self.key_sequence_tracker.reset(); + match key.code { + KeyCode::Esc => { + self.command_mode = false; + self.command_input.clear(); + self.command_message.clear(); + } + _ => { + if !self.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); + self.command_message = format!("Read-only mode - press {} to edit", edit_key); + } } } } @@ -380,6 +278,137 @@ impl EventHandler { Ok((false, self.command_message.clone())) } + fn execute_action( + &mut self, + action: &str, + form_state: &mut FormState, + ) -> Result> { + match action { + "move_left" => { + form_state.current_cursor_pos = form_state.current_cursor_pos.saturating_sub(1); + self.ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + } + "move_right" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() - 1 { + form_state.current_cursor_pos += 1; + } + self.ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + }, + "move_up" => { + // Change field first + if form_state.current_field == 0 { + form_state.current_field = form_state.fields.len() - 1; + } else { + form_state.current_field = form_state.current_field.saturating_sub(1); + } + + // Get current input AFTER changing field + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !current_input.is_empty() { + current_input.len() - 1 + } else { + 0 + }; + form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); + Ok("".to_string()) + } + "move_down" => { + // Change field first + form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); + + // Get current input AFTER changing field + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !current_input.is_empty() { + current_input.len() - 1 + } else { + 0 + }; + form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); + Ok("".to_string()) + } + "move_word_next" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_next_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + self.ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_end" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + self.ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_prev_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_end_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_prev_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("Moved to previous word end".to_string()) + } + "move_line_start" => { + form_state.current_cursor_pos = 0; + self.ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + } + "move_line_end" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + form_state.current_cursor_pos = current_input.len() - 1; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_first_line" => { + // Change field first + form_state.current_field = 0; + + // Get current input AFTER changing field + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { + current_input.len() - 1 + } else { + current_input.len() + }; + form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); + Ok("Moved to first line".to_string()) + } + "move_last_line" => { + // Change field first + form_state.current_field = form_state.fields.len() - 1; + + // Get current input AFTER changing field + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { + current_input.len() - 1 + } else { + current_input.len() + }; + form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); + Ok("Moved to last line".to_string()) + } + _ => Ok("Unknown action".to_string()), + } + } // Helper function to determine character type fn get_char_type(c: char) -> CharType {