// src/client/ui/handlers/event.rs use crossterm::event::{Event, KeyCode, KeyModifiers}; use crossterm::cursor::{SetCursorStyle}; use crate::terminal::AppTerminal; use crate::config::Config; use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest}; use super::form::FormState; pub struct EventHandler { pub command_mode: bool, pub command_input: String, pub command_message: String, pub is_edit_mode: bool, pub edit_mode_cooldown: bool, pub ideal_cursor_column: usize, } #[derive(PartialEq)] enum CharType { Whitespace, Alphanumeric, Punctuation, } impl EventHandler { pub fn new() -> Self { EventHandler { command_mode: false, command_input: String::new(), command_message: String::new(), is_edit_mode: false, edit_mode_cooldown: false, ideal_cursor_column: 0, } } pub async fn handle_event( &mut self, event: Event, config: &Config, app_terminal: &mut AppTerminal, form_state: &mut FormState, is_saved: &mut bool, total_count: u64, current_position: &mut u64, ) -> Result<(bool, String), Box> { if let Event::Key(key) = event { 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; 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; } return Ok((false, self.command_message.clone())); } if !self.is_edit_mode { // 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) => { // 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, 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 // In readonly mode, limit to last character } 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); } } 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) => { // 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, 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 // In readonly mode, limit to last character } 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 { // 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(); } 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())); } "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; } 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())); } _ => {} } } // Handle other keys (e.g., command mode) match key.code { KeyCode::Char(':') => { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); } 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 { // Edit mode handling if self.command_mode { match key.code { KeyCode::Enter => { let command = self.command_input.trim(); if command.is_empty() { self.command_message = "Empty command".to_string(); return Ok((false, self.command_message.clone())); } let action = config.get_action_for_command(command) .unwrap_or("unknown"); if action == "save" { let is_new = *current_position == total_count + 1; let message = if is_new { // POST new entry let post_request = PostAdresarRequest { firma: form_state.values[0].clone(), kz: form_state.values[1].clone(), drc: form_state.values[2].clone(), ulica: form_state.values[3].clone(), psc: form_state.values[4].clone(), mesto: form_state.values[5].clone(), stat: form_state.values[6].clone(), banka: form_state.values[7].clone(), ucet: form_state.values[8].clone(), skladm: form_state.values[9].clone(), ico: form_state.values[10].clone(), kontakt: form_state.values[11].clone(), telefon: form_state.values[12].clone(), skladu: form_state.values[13].clone(), fax: form_state.values[14].clone(), }; let response = app_terminal.post_adresar(post_request).await?; // Update state let new_total = app_terminal.get_adresar_count().await?; *current_position = new_total; form_state.id = response.into_inner().id; "New entry created".to_string() } else { // PUT existing entry let put_request = PutAdresarRequest { id: form_state.id, firma: form_state.values[0].clone(), kz: form_state.values[1].clone(), drc: form_state.values[2].clone(), ulica: form_state.values[3].clone(), psc: form_state.values[4].clone(), mesto: form_state.values[5].clone(), stat: form_state.values[6].clone(), banka: form_state.values[7].clone(), ucet: form_state.values[8].clone(), skladm: form_state.values[9].clone(), ico: form_state.values[10].clone(), kontakt: form_state.values[11].clone(), telefon: form_state.values[12].clone(), skladu: form_state.values[13].clone(), fax: form_state.values[14].clone(), }; let _ = app_terminal.put_adresar(put_request).await?; "Entry updated".to_string() }; *is_saved = true; form_state.has_unsaved_changes = false; self.command_input.clear(); // Clear the command input self.command_mode = false; // Reset command mode self.command_message.clear(); // Clear the command message return Ok((false, message)); } else { let (should_exit, message) = app_terminal .handle_command(action, is_saved) .await?; self.command_message = message; self.command_input.clear(); // Clear the command input self.command_mode = false; // Reset command mode return Ok((should_exit, self.command_message.clone())); } } KeyCode::Char(c) => self.command_input.push(c), KeyCode::Backspace => { self.command_input.pop(); } KeyCode::Esc => { self.command_mode = false; self.command_input.clear(); self.command_message.clear(); } _ => {} } } else { // Handle arrow keys in edit mode match key.code { KeyCode::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())); } KeyCode::Right => { let current_input = form_state.get_current_input(); if form_state.current_cursor_pos < current_input.len() { form_state.current_cursor_pos += 1; self.ideal_cursor_column = form_state.current_cursor_pos; } return Ok((false, "".to_string())); } KeyCode::Char(':') => { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); } KeyCode::Esc => { if 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(); // ADD THIS CODE RIGHT HERE: 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; } return Ok((false, self.command_message.clone())); } }, KeyCode::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 !self.is_edit_mode && !current_input.is_empty() { current_input.len() - 1 // In readonly mode, limit to last character } else { current_input.len() }; form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); } KeyCode::Up => { 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); } 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 } else { current_input.len() }; form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); } KeyCode::Tab => { if key.modifiers.contains(KeyModifiers::SHIFT) { 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); } } else { 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 !self.is_edit_mode && !current_input.is_empty() { current_input.len() - 1 // In readonly mode, limit to last character } else { current_input.len() }; form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); } KeyCode::BackTab => { 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); } 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 } else { current_input.len() }; form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); } KeyCode::Enter => { 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 !self.is_edit_mode && !current_input.is_empty() { current_input.len() - 1 // In readonly mode, limit to last character } else { current_input.len() }; form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); } KeyCode::Char(c) => { let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); if cursor_pos <= chars.len() { chars.insert(cursor_pos, c); *field_value = chars.into_iter().collect(); form_state.current_cursor_pos = cursor_pos + 1; self.ideal_cursor_column = form_state.current_cursor_pos; form_state.has_unsaved_changes = true; } } KeyCode::Backspace => { if form_state.current_cursor_pos > 0 { let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); if cursor_pos <= chars.len() && cursor_pos > 0 { chars.remove(cursor_pos - 1); *field_value = chars.into_iter().collect(); form_state.current_cursor_pos = cursor_pos - 1; self.ideal_cursor_column = form_state.current_cursor_pos; form_state.has_unsaved_changes = true; } } } KeyCode::Delete => { let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); let chars: Vec = field_value.chars().collect(); if cursor_pos < chars.len() { let mut new_chars = chars.clone(); new_chars.remove(cursor_pos); *field_value = new_chars.into_iter().collect(); form_state.has_unsaved_changes = true; } } _ => {} } } } } self.edit_mode_cooldown = false; Ok((false, self.command_message.clone())) } // 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 } } // 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 } }