diff --git a/client/config.toml b/client/config.toml index b79d565..ac169dc 100644 --- a/client/config.toml +++ b/client/config.toml @@ -8,6 +8,7 @@ save_and_quit = [":wq", "ctrl+shift+s"] # MODE SPECIFIC # READ ONLY MODE +[keybindings.read_only] enter_edit_mode_before = ["i"] enter_edit_mode_after = ["a"] previous_entry = ["left","q"] @@ -26,14 +27,14 @@ move_line_end = ["$"] move_first_line = ["gg"] move_last_line = ["x"] -# EDIT MODE +[keybindings.edit] exit_edit_mode = ["esc","ctrl+e"] delete_char_forward = ["delete"] delete_char_backward = ["backspace"] next_field = ["tab", "enter"] prev_field = ["shift+tab", "backtab"] -# COMMAND MODE +[keybindings.command] enter_command_mode = [":", "ctrl+;"] exit_command_mode = ["ctrl+g", "esc"] command_execute = ["enter"] diff --git a/client/src/config/config.rs b/client/src/config/config.rs index 968c739..22eef9f 100644 --- a/client/src/config/config.rs +++ b/client/src/config/config.rs @@ -17,16 +17,25 @@ fn default_theme() -> String { #[derive(Debug, Deserialize)] pub struct Config { - pub keybindings: HashMap>, + #[serde(rename = "keybindings")] + pub keybindings: ModeKeybindings, #[serde(default)] pub colors: ColorsConfig, } +#[derive(Debug, Deserialize)] +pub struct ModeKeybindings { + #[serde(default)] + pub read_only: HashMap>, + #[serde(default)] + pub edit: HashMap>, + #[serde(default)] + pub command: HashMap>, +} + impl Config { /// Loads the configuration from "config.toml" in the client crate directory. pub fn load() -> Result> { - // env!("CARGO_MANIFEST_DIR") resolves to the directory where the Cargo.toml of - // the client crate is located. let manifest_dir = env!("CARGO_MANIFEST_DIR"); let config_path = Path::new(manifest_dir).join("config.toml"); let config_str = std::fs::read_to_string(&config_path) @@ -35,32 +44,45 @@ impl Config { Ok(config) } - pub fn get_action_for_key( + /// Gets an action for a key in Read-Only mode. + pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { + self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers) + } + + /// Gets an action for a key in Edit mode. + pub fn get_edit_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { + self.get_action_for_key_in_mode(&self.keybindings.edit, key, modifiers) + } + + /// Gets an action for a key in Command mode. + pub fn get_command_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { + self.get_action_for_key_in_mode(&self.keybindings.command, key, modifiers) + } + + /// Helper function to get an action for a key in a specific mode. + fn get_action_for_key_in_mode( &self, + mode_bindings: &HashMap>, key: KeyCode, modifiers: KeyModifiers, ) -> Option<&str> { - for (action, bindings) in &self.keybindings { - // Skip mode toggle actions - if action == "enter_edit_mode" || action == "exit_edit_mode" { - continue; - } + for (action, bindings) in mode_bindings { for binding in bindings { if Self::matches_keybinding(binding, key, modifiers) { - return Some(action); + return Some(action.as_str()); } } } None } - /// Checks if a sequence of keys matches any keybinding + /// 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) + + // 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) @@ -68,46 +90,56 @@ impl Config { None } }).collect(); - + if sequence_str.is_empty() { return None; } - - // Check if this sequence matches any binding - for (action, bindings) in &self.keybindings { + + // Check if this sequence matches any binding. + for (action, bindings) in &self.keybindings.read_only { for binding in bindings { - // Skip bindings with modifiers (those contain '+') - if binding.contains('+') { - continue; - } - if binding == &sequence_str { return Some(action); } } } - + for (action, bindings) in &self.keybindings.edit { + for binding in bindings { + if binding == &sequence_str { + return Some(action); + } + } + } + for (action, bindings) in &self.keybindings.command { + for binding in bindings { + if binding == &sequence_str { + return Some(action); + } + } + } + None } + /// Checks if a keybinding matches a key and modifiers. fn matches_keybinding( binding: &str, key: KeyCode, modifiers: KeyModifiers, ) -> bool { - // For multi-character bindings without modifiers, we handle them in matches_key_sequence + // For multi-character bindings without modifiers, handle them in matches_key_sequence. if binding.len() > 1 && !binding.contains('+') { - return match binding.to_lowercase().as_str() { - "left" => key == KeyCode::Left, - "right" => key == KeyCode::Right, - "up" => key == KeyCode::Up, - "down" => key == KeyCode::Down, - "esc" => key == KeyCode::Esc, - "enter" => key == KeyCode::Enter, - _ => false, - }; + return match binding.to_lowercase().as_str() { + "left" => key == KeyCode::Left, + "right" => key == KeyCode::Right, + "up" => key == KeyCode::Up, + "down" => key == KeyCode::Down, + "esc" => key == KeyCode::Esc, + "enter" => key == KeyCode::Enter, + _ => false, + }; } - + let parts: Vec<&str> = binding.split('+').collect(); let mut expected_modifiers = KeyModifiers::empty(); let mut expected_key = None; @@ -136,8 +168,9 @@ impl Config { modifiers == expected_modifiers && Some(key) == expected_key } + /// Gets an action for a command string. pub fn get_action_for_command(&self, command: &str) -> Option<&str> { - for (action, bindings) in &self.keybindings { + for (action, bindings) in &self.keybindings.command { for binding in bindings { if binding.starts_with(':') && binding.trim_start_matches(':') == command { return Some(action); @@ -147,51 +180,76 @@ impl Config { None } + /// Checks if a key is bound to entering Edit mode (before cursor). pub fn is_enter_edit_mode_before(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("enter_edit_mode_before") { + if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_before") { bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) } else { false } } + /// Checks if a key is bound to entering Edit mode (after cursor). pub fn is_enter_edit_mode_after(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("enter_edit_mode_after") { + if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_after") { bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) } else { false } } + /// Checks if a key is bound to entering Edit mode. pub fn is_enter_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - self.is_enter_edit_mode_before(key, modifiers) || - self.is_enter_edit_mode_after(key, modifiers) + self.is_enter_edit_mode_before(key, modifiers) || self.is_enter_edit_mode_after(key, modifiers) } + /// Checks if a key is bound to exiting Edit mode. pub fn is_exit_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("exit_edit_mode") { + if let Some(bindings) = self.keybindings.edit.get("exit_edit_mode") { bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) } else { false } } + /// Checks if a key is bound to entering Command mode. pub fn is_enter_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("enter_command_mode") { + if let Some(bindings) = self.keybindings.command.get("enter_command_mode") { bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) } else { false } } + /// Checks if a key is bound to exiting Command mode. pub fn is_exit_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("exit_command_mode") { + if let Some(bindings) = self.keybindings.command.get("exit_command_mode") { bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) } else { false } } + /// Checks if a key is bound to executing a command. + pub fn is_command_execute(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { + if let Some(bindings) = self.keybindings.command.get("command_execute") { + bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) + } else { + // Fall back to Enter key if no command_execute is defined. + key == KeyCode::Enter && modifiers.is_empty() + } + } + + /// Checks if a key is bound to backspacing in Command mode. + pub fn is_command_backspace(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { + if let Some(bindings) = self.keybindings.command.get("command_backspace") { + bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) + } else { + // Fall back to Backspace key if no command_backspace is defined. + key == KeyCode::Backspace && modifiers.is_empty() + } + } + /// Checks if a key is bound to a specific action. pub fn has_key_for_action(&self, action: &str, key_char: char) -> bool { if let Some(bindings) = self.keybindings.get(action) { for binding in bindings { @@ -203,7 +261,7 @@ impl Config { false } - // This method handles all keybinding formats, both with and without + + /// 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; @@ -255,7 +313,7 @@ impl Config { None } - // Check if the current key sequence is a prefix of a longer binding + /// 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; @@ -300,22 +358,4 @@ impl Config { false } - - pub fn is_command_execute(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("command_execute") { - bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) - } else { - // Fall back to Enter key if no command_execute is defined - key == KeyCode::Enter && modifiers.is_empty() - } - } - - pub fn is_command_backspace(&self, key: KeyCode, modifiers: KeyModifiers) -> bool { - if let Some(bindings) = self.keybindings.get("command_backspace") { - bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers)) - } else { - // Fall back to Backspace key if no command_backspace is defined - key == KeyCode::Backspace && modifiers.is_empty() - } - } } diff --git a/client/src/modes/handlers/edit.rs b/client/src/modes/handlers/edit.rs index 1fdf75b..0bf924b 100644 --- a/client/src/modes/handlers/edit.rs +++ b/client/src/modes/handlers/edit.rs @@ -14,52 +14,16 @@ pub async fn handle_edit_event_internal( ideal_cursor_column: &mut usize, command_message: &mut String, ) -> Result> { - // Try to match against configured action mappings first - let mut key_sequence_tracker = KeySequenceTracker::new(800); - let mut handled_by_config = false; - - if key.modifiers.is_empty() { - key_sequence_tracker.add_key(key.code); - let sequence = key_sequence_tracker.get_sequence(); - - // Try to match the current sequence against all bindings - if let Some(action) = config.matches_key_sequence_generalized(&sequence) { - return execute_edit_action( - action, - form_state, - ideal_cursor_column, - ).await; - } - - // 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(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) { - return execute_edit_action( - action, - form_state, - ideal_cursor_column, - ).await; - } - } - } else { - // If modifiers are pressed, check for direct key bindings - if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { - return execute_edit_action( - action, - form_state, - ideal_cursor_column, - ).await; - } + // Try to match against configured Edit mode action mappings first + if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { + return execute_edit_action( + action, + form_state, + ideal_cursor_column, + ).await; } - // If we get here, no key mapping was found, so we handle fallback behavior + // If no Edit mode action is found, handle fallback behavior handle_edit_specific_input( key, form_state, @@ -69,39 +33,6 @@ pub async fn handle_edit_event_internal( Ok(command_message.clone()) } -// Original handle_edit_event for backward compatibility - not actually used anymore -// but kept for API compatibility -pub async fn handle_edit_event( - key: KeyEvent, - config: &Config, - form_state: &mut FormState, - _is_edit_mode: &mut bool, - edit_mode_cooldown: &mut bool, - ideal_cursor_column: &mut usize, - command_message: &mut String, - _command_mode: &mut bool, - _command_input: &mut String, - _app_terminal: &mut AppTerminal, - _is_saved: &mut bool, - _current_position: &mut u64, - _total_count: u64, -) -> Result<(bool, String), Box> { - // This function should ideally not be called anymore - everything should go through event.rs - // Log a warning that this code path is deprecated - eprintln!("Warning: Deprecated code path in handle_edit_event - use event.rs instead"); - - let result = handle_edit_event_internal( - key, - config, - form_state, - ideal_cursor_column, - command_message, - ).await?; - - *edit_mode_cooldown = false; - Ok((false, result)) -} - // Handle edit-specific key input as a fallback (character input, backspace, delete) fn handle_edit_specific_input( key: KeyEvent, diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 4ecf415..36c850d 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -78,7 +78,7 @@ impl EventHandler { // Mode transitions between edit mode and read-only mode if self.is_edit_mode { // Check for exiting edit mode - if config.is_exit_edit_mode(key.code, key.modifiers) { + if config.get_edit_action_for_key(key.code, key.modifiers) == Some("exit_edit_mode") { if form_state.has_unsaved_changes { self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string(); return Ok((false, self.command_message.clone())); @@ -109,8 +109,8 @@ impl EventHandler { return Ok((false, result)); } else { // Check for entering edit mode from read-only mode - if config.is_enter_edit_mode(key.code, key.modifiers) { - if config.is_enter_edit_mode_after(key.code, key.modifiers) { + if config.get_read_only_action_for_key(key.code, key.modifiers) == Some("enter_edit_mode_before") { + if config.get_read_only_action_for_key(key.code, key.modifiers) == Some("enter_edit_mode_after") { 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; diff --git a/client/src/modes/handlers/read_only.rs b/client/src/modes/handlers/read_only.rs index a95357a..a15ed15 100644 --- a/client/src/modes/handlers/read_only.rs +++ b/client/src/modes/handlers/read_only.rs @@ -1,5 +1,3 @@ -// src/modes/handlers/read_only.rs - use crossterm::event::{KeyEvent}; use crate::config::config::Config; use crate::ui::handlers::form::FormState; @@ -13,7 +11,6 @@ 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, @@ -26,13 +23,30 @@ pub async fn handle_read_only_event( edit_mode_cooldown: &mut bool, ideal_cursor_column: &mut usize, ) -> Result<(bool, String), Box> { - // Always add the key to the sequence tracker if no modifiers - // This tracks ALL keys, not just character keys + // Check for entering Edit mode from Read-Only mode + if config.get_read_only_action_for_key(key.code, key.modifiers) == Some("enter_edit_mode_before") { + *edit_mode_cooldown = true; + *command_message = "Entering Edit mode".to_string(); + return Ok((false, command_message.clone())); + } + + if config.get_read_only_action_for_key(key.code, key.modifiers) == Some("enter_edit_mode_after") { + 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; + *ideal_cursor_column = form_state.current_cursor_pos; + } + *edit_mode_cooldown = true; + *command_message = "Entering Edit mode (after cursor)".to_string(); + return Ok((false, command_message.clone())); + } + + // Handle Read-Only mode keybindings if key.modifiers.is_empty() { key_sequence_tracker.add_key(key.code); let sequence = key_sequence_tracker.get_sequence(); - // Try to match the current sequence against all bindings + // Try to match the current sequence against Read-Only mode bindings if let Some(action) = config.matches_key_sequence_generalized(&sequence) { let result = execute_action( action, @@ -50,14 +64,12 @@ pub async fn handle_read_only_event( // 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) { + if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { let result = execute_action( action, form_state, @@ -76,7 +88,7 @@ pub async fn handle_read_only_event( // If modifiers are pressed, check for direct key bindings key_sequence_tracker.reset(); - if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { + if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { let result = execute_action( action, form_state, @@ -94,7 +106,7 @@ pub async fn handle_read_only_event( // 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") + let edit_key = config.keybindings.read_only.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);