diff --git a/client/config.toml b/client/config.toml index e5ead44..9032f47 100644 --- a/client/config.toml +++ b/client/config.toml @@ -8,7 +8,7 @@ enter_edit_mode_before = ["i"] enter_edit_mode_after = ["a"] exit_edit_mode = ["esc", "ctrl+e"] previous_position = ["Left", "9"] -next_position = ["Right", "8"] +next_position = ["Right"] 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 = ["x"] # Move to last line of form +move_last_line = ["x", "8"] [colors] theme = "dark" diff --git a/client/src/config/key_sequences.rs b/client/src/config/key_sequences.rs index b0bea5e..6cd33b9 100644 --- a/client/src/config/key_sequences.rs +++ b/client/src/config/key_sequences.rs @@ -1,3 +1,4 @@ +// client/src/key_sequences.rs use crossterm::event::{KeyCode, KeyModifiers}; use std::time::{Duration, Instant}; diff --git a/client/src/modes/handlers.rs b/client/src/modes/handlers.rs index 314825b..f4609c9 100644 --- a/client/src/modes/handlers.rs +++ b/client/src/modes/handlers.rs @@ -2,3 +2,4 @@ pub mod event; pub mod edit; pub mod command_mode; +pub mod read_only; diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 8277365..c2a34fa 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -1,13 +1,12 @@ // src/modes/handlers/event.rs -use crossterm::event::{Event, KeyCode}; -use crossterm::cursor::{SetCursorStyle}; +use crossterm::event::{Event, KeyCode, KeyEvent}; +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::modes::handlers::{edit, command_mode, read_only}; use crate::config::key_sequences::KeySequenceTracker; -use crate::modes::handlers::command_mode::handle_command_event; pub struct EventHandler { pub command_mode: bool, @@ -19,13 +18,6 @@ pub struct EventHandler { pub key_sequence_tracker: KeySequenceTracker, } -#[derive(PartialEq)] -enum CharType { - Whitespace, - Alphanumeric, - Punctuation, -} - impl EventHandler { pub fn new() -> Self { EventHandler { @@ -50,234 +42,30 @@ impl EventHandler { current_position: &mut u64, ) -> Result<(bool, String), Box> { if let Event::Key(key) = event { - // Handle command mode first, regardless of the current editing mode + // Handle command mode first if self.command_mode { - let (should_exit, message, exit_command_mode) = handle_command_event( - key, - config, - form_state, - &mut self.command_input, - &mut self.command_message, - app_terminal, - is_saved, - current_position, - total_count, - ).await?; - - if exit_command_mode { - self.command_mode = false; - } - - if !message.is_empty() { - return Ok((should_exit, message)); - } - - if self.command_mode { - return Ok((false, "".to_string())); - } - } - 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())); + return self.handle_command_mode( + key, config, app_terminal, form_state, + is_saved, current_position, total_count + ).await; } // Handle mode transitions - if !self.is_edit_mode && config.is_enter_edit_mode(key.code, key.modifiers) { - if config.is_enter_edit_mode_after(key.code, key.modifiers) { - 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; - self.ideal_cursor_column = form_state.current_cursor_pos; - } - } - self.is_edit_mode = true; - self.edit_mode_cooldown = true; - self.command_message = "Edit mode".to_string(); - app_terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; - return Ok((false, self.command_message.clone())); - } else if self.is_edit_mode && config.is_exit_edit_mode(key.code, key.modifiers) { - 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())); - } - self.is_edit_mode = false; - self.edit_mode_cooldown = true; - self.command_message = "Read-only mode".to_string(); - app_terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; - - 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 = current_input.len() - 1; - self.ideal_cursor_column = form_state.current_cursor_pos; - } - + if self.handle_mode_transitions(key, config, app_terminal, form_state)? { return Ok((false, self.command_message.clone())); } + // Delegate to appropriate mode handler if self.is_edit_mode { - let result = handle_edit_event( - key, - config, - form_state, - &mut self.is_edit_mode, - &mut self.edit_mode_cooldown, - &mut self.ideal_cursor_column, - &mut self.command_message, - &mut self.command_mode, - &mut self.command_input, - app_terminal, - is_saved, - current_position, - total_count, + return self.handle_edit_mode( + key, config, app_terminal, form_state, + is_saved, current_position, total_count ).await; - self.key_sequence_tracker.reset(); - return result; } else { - // Handle navigation between entries - if key.code == KeyCode::Left { - let new_position = current_position.saturating_sub(1); - if new_position >= 1 { - *current_position = new_position; - match app_terminal.get_adresar_by_position(*current_position).await { - Ok(response) => { - form_state.id = response.id; - form_state.values = vec![ - response.firma, - response.kz, - response.drc, - response.ulica, - response.psc, - response.mesto, - response.stat, - response.banka, - response.ucet, - response.skladm, - response.ico, - response.kontakt, - response.telefon, - response.skladu, - response.fax, - ]; - - 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); - form_state.has_unsaved_changes = false; - self.command_message = format!("Loaded entry {}", *current_position); - } - Err(e) => { - 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 { - if *current_position <= total_count { - *current_position += 1; - - if *current_position <= total_count { - match app_terminal.get_adresar_by_position(*current_position).await { - Ok(response) => { - form_state.id = response.id; - form_state.values = vec![ - response.firma, - response.kz, - response.drc, - response.ulica, - response.psc, - response.mesto, - response.stat, - response.banka, - response.ucet, - response.skladm, - response.ico, - response.kontakt, - response.telefon, - response.skladu, - response.fax, - ]; - - 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); - form_state.has_unsaved_changes = false; - self.command_message = format!("Loaded entry {}", *current_position); - } - Err(e) => { - self.command_message = format!("Error loading entry: {}", e); - } - } - } else { - 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 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(); - let sequence_str = self.key_sequence_tracker.sequence_to_string(); - - // 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)); - } - - // Only execute single-key actions if this key doesn't begin any multi-key sequence - // This prevents "g" from triggering an action when it could be the start of "gg" or "ge" - if sequence.len() == 1 { - let is_prefix_of_multikey = self.is_prefix_of_multikey_binding(&sequence_str, config); - - if !is_prefix_of_multikey { - if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { - let result = self.execute_action(action, form_state)?; - self.key_sequence_tracker.reset(); // Reset after executing - return Ok((false, result)); - } - } - } - } else { - self.key_sequence_tracker.reset(); - } - } 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); - } - } - } - } - } + return self.handle_read_only_mode( + key, config, app_terminal, form_state, + current_position, total_count + ).await; } } @@ -285,308 +73,141 @@ impl EventHandler { Ok((false, self.command_message.clone())) } - // Add this helper method to check if a key sequence is a prefix of any multi-key binding - fn is_prefix_of_multikey_binding(&self, sequence: &str, config: &Config) -> bool { - for (_, bindings) in &config.keybindings { - for binding in bindings { - // Skip bindings with modifiers (those contain '+') - if binding.contains('+') { - continue; - } - - // Check if binding starts with our sequence and is longer - if binding.len() > sequence.len() && binding.starts_with(sequence) { - return true; - } - } - } - false - } - - fn execute_action( + async fn handle_command_mode( &mut self, - action: &str, + key: KeyEvent, + config: &Config, + app_terminal: &mut AppTerminal, 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" => { + is_saved: &mut bool, + current_position: &mut u64, + total_count: u64, + ) -> Result<(bool, String), Box> { + let (should_exit, message, exit_command_mode) = command_mode::handle_command_event( + key, + config, + form_state, + &mut self.command_input, + &mut self.command_message, + app_terminal, + is_saved, + current_position, + total_count, + ).await?; + + if exit_command_mode { + self.command_mode = false; + } + + if !message.is_empty() { + return Ok((should_exit, message)); + } + + Ok((false, "".to_string())) + } + + fn handle_mode_transitions( + &mut self, + key: KeyEvent, + config: &Config, + app_terminal: &mut AppTerminal, + form_state: &mut FormState, + ) -> Result> { + // Enter command mode + if !self.is_edit_mode && key.code == KeyCode::Char(':') { + self.command_mode = true; + self.command_input.clear(); + self.command_message.clear(); + return Ok(true); + } + + // Enter edit mode + if !self.is_edit_mode && config.is_enter_edit_mode(key.code, key.modifiers) { + if config.is_enter_edit_mode_after(key.code, key.modifiers) { let current_input = form_state.get_current_input(); - if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() - 1 { + if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() { form_state.current_cursor_pos += 1; + self.ideal_cursor_column = form_state.current_cursor_pos; } + } + self.is_edit_mode = true; + self.edit_mode_cooldown = true; + self.command_message = "Edit mode".to_string(); + app_terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?; + return Ok(true); + } + + // Exit edit mode + if self.is_edit_mode && config.is_exit_edit_mode(key.code, key.modifiers) { + if form_state.has_unsaved_changes { + self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string(); + return Ok(true); + } + self.is_edit_mode = false; + self.edit_mode_cooldown = true; + self.command_message = "Read-only mode".to_string(); + app_terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; + + 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 = current_input.len() - 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()), + return Ok(true); } + + Ok(false) } - // Helper function to determine character type - fn get_char_type(c: char) -> CharType { - if c.is_whitespace() { - CharType::Whitespace - } else if c.is_alphanumeric() { - CharType::Alphanumeric - } else { - CharType::Punctuation - } + async fn handle_edit_mode( + &mut self, + key: KeyEvent, + config: &Config, + app_terminal: &mut AppTerminal, + form_state: &mut FormState, + is_saved: &mut bool, + current_position: &mut u64, + total_count: u64, + ) -> Result<(bool, String), Box> { + let result = edit::handle_edit_event( + key, + config, + form_state, + &mut self.is_edit_mode, + &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + &mut self.command_message, + &mut self.command_mode, + &mut self.command_input, + app_terminal, + is_saved, + current_position, + total_count, + ).await; + self.key_sequence_tracker.reset(); + result } - // Find the beginning of the next word from current position - fn find_next_word_start(&self, text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - if chars.is_empty() || current_pos >= chars.len() { - return current_pos; - } - - let mut pos = current_pos; - - // Step 1: Skip current word/set of similar characters - let initial_type = Self::get_char_type(chars[pos]); - while pos < chars.len() && Self::get_char_type(chars[pos]) == initial_type { - pos += 1; - } - - // Step 2: Skip any whitespace - while pos < chars.len() && Self::get_char_type(chars[pos]) == CharType::Whitespace { - pos += 1; - } - - pos - } - - // Find the end of the current/next word - fn find_word_end(&self, text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - if chars.is_empty() { - return 0; - } - - // If already at the end of the text, stay there - if current_pos >= chars.len() - 1 { - return chars.len() - 1; - } - - let mut pos = current_pos; - - // If we're on whitespace, move forward to the next word - if Self::get_char_type(chars[pos]) == CharType::Whitespace { - // Skip whitespace to find the next word - while pos < chars.len() && Self::get_char_type(chars[pos]) == CharType::Whitespace { - pos += 1; - } - } else { - // We're on a word character - check if we're at the end of the current word - let current_type = Self::get_char_type(chars[pos]); - if pos + 1 < chars.len() && Self::get_char_type(chars[pos + 1]) != current_type { - // We're at the end of the current word, so move to the next word - pos += 1; - // Skip any whitespace - while pos < chars.len() && Self::get_char_type(chars[pos]) == CharType::Whitespace { - pos += 1; - } - } - } - - // If we've reached the end of the text, return the last position - if pos >= chars.len() { - return chars.len() - 1; - } - - // Get the type of the word we're now on - let word_type = Self::get_char_type(chars[pos]); - - // Move to the end of this word - while pos + 1 < chars.len() && Self::get_char_type(chars[pos + 1]) == word_type { - pos += 1; - } - - pos - } - - // Find the beginning of the previous word - fn find_prev_word_start(&self, text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - if chars.is_empty() || current_pos == 0 { - return 0; - } - - let mut pos = current_pos.saturating_sub(1); - - // Step 1: Skip any whitespace backward - while pos > 0 && Self::get_char_type(chars[pos]) == CharType::Whitespace { - pos -= 1; - } - - if Self::get_char_type(chars[pos]) != CharType::Whitespace { - // Step 2: Find the beginning of this word - let word_type = Self::get_char_type(chars[pos]); - while pos > 0 && Self::get_char_type(chars[pos - 1]) == word_type { - pos -= 1; - } - } - - pos - } - - // Find the end of the previous word - fn find_prev_word_end(&self, text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - if chars.is_empty() || current_pos <= 1 { - return 0; - } - - let mut pos = current_pos.saturating_sub(1); - - // Step 1: Skip any whitespace backward - while pos > 0 && Self::get_char_type(chars[pos]) == CharType::Whitespace { - pos -= 1; - } - - // If we hit a non-whitespace character, find the beginning of this word - if pos > 0 && Self::get_char_type(chars[pos]) != CharType::Whitespace { - let word_type = Self::get_char_type(chars[pos]); - - // Step 2: Skip backward past this word - while pos > 0 && Self::get_char_type(chars[pos - 1]) == word_type { - pos -= 1; - } - - // Step 3: Skip whitespace before this word - while pos > 0 && Self::get_char_type(chars[pos - 1]) == CharType::Whitespace { - pos -= 1; - } - - // Step 4: Find end of previous word - if pos > 0 { - pos -= 1; - let prev_word_type = Self::get_char_type(chars[pos]); - while pos > 0 && Self::get_char_type(chars[pos - 1]) == prev_word_type { - pos -= 1; - } - - // Find the end of this word - while pos < chars.len() - 1 && - Self::get_char_type(chars[pos + 1]) == prev_word_type { - pos += 1; - } - } - } - - pos + async fn handle_read_only_mode( + &mut self, + key: KeyEvent, + config: &Config, + app_terminal: &mut AppTerminal, + form_state: &mut FormState, + current_position: &mut u64, + total_count: u64, + ) -> Result<(bool, String), Box> { + read_only::handle_read_only_event( + key, + config, + form_state, + &mut self.key_sequence_tracker, + current_position, + total_count, + app_terminal, + &mut self.command_message, + &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + ).await } } diff --git a/client/src/modes/handlers/read_only.rs b/client/src/modes/handlers/read_only.rs new file mode 100644 index 0000000..ad335c4 --- /dev/null +++ b/client/src/modes/handlers/read_only.rs @@ -0,0 +1,422 @@ +// src/modes/handlers/read_only.rs + +use crossterm::event::{KeyEvent, KeyCode}; +use crate::config::config::Config; +use crate::ui::handlers::form::FormState; +use crate::config::key_sequences::KeySequenceTracker; +use crate::tui::terminal::AppTerminal; + +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + +pub async fn handle_read_only_event( + key: KeyEvent, + config: &Config, + form_state: &mut FormState, + key_sequence_tracker: &mut KeySequenceTracker, + current_position: &mut u64, + total_count: u64, + app_terminal: &mut AppTerminal, + command_message: &mut String, + edit_mode_cooldown: &mut bool, + ideal_cursor_column: &mut usize, +) -> Result<(bool, String), Box> { + // Handle navigation between entries + match key.code { + KeyCode::Left => { + let new_position = current_position.saturating_sub(1); + if new_position >= 1 { + *current_position = new_position; + match app_terminal.get_adresar_by_position(*current_position).await { + Ok(response) => { + form_state.id = response.id; + form_state.values = vec![ + response.firma, response.kz, response.drc, + response.ulica, response.psc, response.mesto, + response.stat, response.banka, response.ucet, + response.skladm, response.ico, response.kontakt, + response.telefon, response.skladu, response.fax, + ]; + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !current_input.is_empty() { + current_input.len() - 1 + } else { 0 }; + // Fix type mismatch by dereferencing and using std::cmp::min + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos); + form_state.has_unsaved_changes = false; + *command_message = format!("Loaded entry {}", *current_position); + } + Err(e) => { + *command_message = format!("Error loading entry: {}", e); + } + } + key_sequence_tracker.reset(); + } + return Ok((false, command_message.clone())); + } + KeyCode::Right => { + if *current_position <= total_count { + *current_position += 1; + if *current_position <= total_count { + match app_terminal.get_adresar_by_position(*current_position).await { + Ok(response) => { + form_state.id = response.id; + form_state.values = vec![ + response.firma, response.kz, response.drc, + response.ulica, response.psc, response.mesto, + response.stat, response.banka, response.ucet, + response.skladm, response.ico, response.kontakt, + response.telefon, response.skladu, response.fax, + ]; + let current_input = form_state.get_current_input(); + let max_cursor_pos = if !current_input.is_empty() { + current_input.len() - 1 + } else { 0 }; + // Fix type mismatch + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos); + form_state.has_unsaved_changes = false; + *command_message = format!("Loaded entry {}", *current_position); + } + Err(e) => { + *command_message = format!("Error loading entry: {}", e); + } + } + } else { + form_state.reset_to_empty(); + form_state.current_field = 0; + form_state.current_cursor_pos = 0; + *ideal_cursor_column = 0; // Reset ideal column as well + *command_message = "New entry mode".to_string(); + } + key_sequence_tracker.reset(); + } + return Ok((false, command_message.clone())); + } + KeyCode::Esc => { + // Reset key sequence tracker and message on Escape + key_sequence_tracker.reset(); + *command_message = "".to_string(); + return Ok((false, "".to_string())); + } + _ => {} + } + + // 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(); + + if let Some(action) = config.matches_key_sequence(&sequence) { + let result = execute_action(action, form_state, ideal_cursor_column)?; + 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.reset(); + return Ok((false, result)); + } + } + } + } else { + key_sequence_tracker.reset(); + } + } else { + key_sequence_tracker.reset(); + + // 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); + } + } + + Ok((false, command_message.clone())) +} + +fn execute_action( + action: &str, + form_state: &mut FormState, + ideal_cursor_column: &mut usize, +) -> Result> { + let current_input = form_state.get_current_input(); + let current_pos = form_state.current_cursor_pos; + + match action { + "move_left" => { + form_state.current_cursor_pos = current_pos.saturating_sub(1); + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + Ok("".to_string()) + } + "move_right" => { + if !current_input.is_empty() && current_pos < current_input.len() - 1 { + form_state.current_cursor_pos += 1; + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + Ok("".to_string()) + } + "move_word_next" => { + if !current_input.is_empty() { + let new_pos = find_next_word_start(current_input, current_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + Ok("".to_string()) + } + "move_word_end" => { + if !current_input.is_empty() { + let new_pos = find_word_end(current_input, current_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + Ok("".to_string()) + } + "move_word_prev" => { + if !current_input.is_empty() { + let new_pos = find_prev_word_start(current_input, current_pos); + form_state.current_cursor_pos = new_pos; + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + Ok("".to_string()) + } + "move_word_end_prev" => { + if !current_input.is_empty() { + let new_pos = find_prev_word_end(current_input, current_pos); + form_state.current_cursor_pos = new_pos; + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + Ok("Moved to previous word end".to_string()) + } + "move_line_start" => { + form_state.current_cursor_pos = 0; + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + Ok("".to_string()) + } + "move_line_end" => { + if !current_input.is_empty() { + form_state.current_cursor_pos = current_input.len() - 1; + *ideal_cursor_column = form_state.current_cursor_pos; // Update ideal column + } + 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 + }; + // Fix type mismatch + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, 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 + }; + // Fix type mismatch + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_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 !current_input.is_empty() { + current_input.len() - 1 + } else { + 0 + }; + // Fix type mismatch + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, 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 !current_input.is_empty() { + current_input.len() - 1 + } else { + 0 + }; + // Fix type mismatch + form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos); + Ok("Moved to last line".to_string()) + } + _ => Ok("Unknown action".to_string()), + } +} + +fn is_prefix_of_multikey_binding(sequence: &str, config: &Config) -> bool { + for (_, bindings) in &config.keybindings { + for binding in bindings { + if binding.contains('+') { continue; } + if binding.len() > sequence.len() && binding.starts_with(sequence) { + return true; + } + } + } + false +} + +fn get_char_type(c: char) -> CharType { + if c.is_whitespace() { + CharType::Whitespace + } else if c.is_alphanumeric() { + CharType::Alphanumeric + } else { + CharType::Punctuation + } +} + +fn find_next_word_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos >= chars.len() { + return current_pos; + } + + let mut pos = current_pos; + let initial_type = get_char_type(chars[pos]); + while pos < chars.len() && get_char_type(chars[pos]) == initial_type { + pos += 1; + } + + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + + pos +} + +fn find_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + + if current_pos >= chars.len() - 1 { + return chars.len() - 1; + } + + let mut pos = current_pos; + + if get_char_type(chars[pos]) == CharType::Whitespace { + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + } else { + let current_type = get_char_type(chars[pos]); + if pos + 1 < chars.len() && get_char_type(chars[pos + 1]) != current_type { + pos += 1; + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + } + } + + if pos >= chars.len() { + return chars.len() - 1; + } + + let word_type = get_char_type(chars[pos]); + while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == word_type { + pos += 1; + } + + pos +} + +fn find_prev_word_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos == 0 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + } + + pos +} + +fn find_prev_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos <= 1 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if pos > 0 && get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = get_char_type(chars[pos]); + + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + + while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { + pos -= 1; + } + + if pos > 0 { + pos -= 1; + let prev_word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == prev_word_type { + pos -= 1; + } + + while pos < chars.len() - 1 && + get_char_type(chars[pos + 1]) == prev_word_type { + pos += 1; + } + } + } + + pos +}