// src/modes/canvas/read_only.rs use crate::config::binds::config::Config; use crate::config::binds::key_sequences::KeySequenceTracker; use crate::services::grpc_client::GrpcClient; use crate::state::canvas_state::CanvasState; // Import the trait use crate::state::pages::auth::AuthState; use crate::state::pages::form::FormState; use crossterm::event::KeyEvent; #[derive(PartialEq)] enum CharType { Whitespace, Alphanumeric, Punctuation, } pub async fn handle_read_only_event( app_state: &crate::state::state::AppState, key: KeyEvent, config: &Config, form_state: &mut FormState, // Keep specific types here for routing auth_state: &mut AuthState, // Keep specific types here for routing key_sequence_tracker: &mut KeySequenceTracker, current_position: &mut u64, // Needed for form actions total_count: u64, // Needed for form actions grpc_client: &mut GrpcClient, // Needed for form actions command_message: &mut String, edit_mode_cooldown: &mut bool, ideal_cursor_column: &mut usize, ) -> Result<(bool, String), Box> { // Check for entering Edit mode from Read-Only mode if config.is_enter_edit_mode_before(key.code, key.modifiers) { *edit_mode_cooldown = true; *command_message = "Entering Edit mode".to_string(); // The actual mode switch happens in event.rs based on this return return Ok((false, command_message.clone())); } if config.is_enter_edit_mode_after(key.code, key.modifiers) { // Use the correct state based on context let (current_input, current_pos) = if app_state.ui.show_login { ( auth_state.get_current_input(), auth_state.current_cursor_pos(), ) } else { ( form_state.get_current_input(), form_state.current_cursor_pos(), ) }; if !current_input.is_empty() && current_pos < current_input.len() { // Update the correct state if app_state.ui.show_login { auth_state.set_current_cursor_pos(current_pos + 1); *ideal_cursor_column = auth_state.current_cursor_pos(); } else { form_state.set_current_cursor_pos(current_pos + 1); *ideal_cursor_column = form_state.current_cursor_pos(); } } *edit_mode_cooldown = true; *command_message = "Entering Edit mode (after cursor)".to_string(); // The actual mode switch happens in event.rs based on this return 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 Read-Only mode bindings if let Some(action) = config.matches_key_sequence_generalized(&sequence) { // Handle context-specific actions first let result = if app_state.ui.show_form && (action == "previous_entry" || action == "next_entry") { // Form-specific navigation crate::tui::functions::form::handle_action( action, form_state, grpc_client, current_position, total_count, ideal_cursor_column, ) .await? } else if app_state.ui.show_login && (action == "move_up" || action == "move_down") { // Login-specific field navigation crate::tui::functions::login::handle_action( action, auth_state, ideal_cursor_column, ) .await? } else if app_state.ui.show_form && (action == "move_up" || action == "move_down") { // Form-specific field navigation (can reuse login handler logic if identical) crate::tui::functions::form::handle_action( action, form_state, grpc_client, // Might not be needed for simple up/down current_position, total_count, ideal_cursor_column, ) .await? } else { // Handle common navigation actions generically if app_state.ui.show_login { execute_action( action, auth_state, // Pass AuthState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } else { execute_action( action, form_state, // Pass FormState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } }; key_sequence_tracker.reset(); return Ok((false, result)); } // Check if this might be a prefix of a longer sequence if config.is_key_sequence_prefix(&sequence) { 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) { if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { // Handle context-specific actions first let result = if app_state.ui.show_form && action == "previous_entry" { // Form-specific navigation crate::tui::functions::form::handle_action( action, form_state, grpc_client, current_position, total_count, ideal_cursor_column, ) .await? } else { // Handle common navigation actions generically if app_state.ui.show_login { execute_action( action, auth_state, // Pass AuthState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } else { execute_action( action, form_state, // Pass FormState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } }; key_sequence_tracker.reset(); return Ok((false, result)); } } } else { // If modifiers are pressed, check for direct key bindings key_sequence_tracker.reset(); if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { // Handle context-specific actions first let result = if app_state.ui.show_form && action == "previous_entry" { // Form-specific navigation crate::tui::functions::form::handle_action( action, form_state, grpc_client, current_position, total_count, ideal_cursor_column, ) .await? } else { // Handle common navigation actions generically if app_state.ui.show_login { execute_action( action, auth_state, // Pass AuthState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } else { execute_action( action, form_state, // Pass FormState ideal_cursor_column, key_sequence_tracker, command_message, ) .await? } }; return Ok((false, result)); } } // Show a helpful message when no binding was found if !*edit_mode_cooldown { let default_key = "i".to_string(); 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); } Ok((false, command_message.clone())) } // Make this function generic over CanvasState async fn execute_action( action: &str, state: &mut S, // Use generic state ideal_cursor_column: &mut usize, key_sequence_tracker: &mut KeySequenceTracker, // Keep for resetting command_message: &mut String, // Keep for clearing ) -> Result> { match action { // These actions are handled outside now based on context "previous_entry" | "next_entry" => { key_sequence_tracker.reset(); Ok(format!( "Action '{}' should be handled by context-specific logic", action )) } // These actions are handled outside now based on context "move_up" | "move_down" => { key_sequence_tracker.reset(); Ok(format!( "Action '{}' should be handled by context-specific logic", action )) } "exit_edit_mode" => { // This action is primarily for Edit mode, but might be bound here too key_sequence_tracker.reset(); command_message.clear(); Ok("".to_string()) } "move_left" => { let current_pos = state.current_cursor_pos(); let new_pos = current_pos.saturating_sub(1); state.set_current_cursor_pos(new_pos); // Use trait setter *ideal_cursor_column = new_pos; Ok("".to_string()) } "move_right" => { let current_input = state.get_current_input(); let current_pos = state.current_cursor_pos(); // In read-only, cursor stops AT the last character, not after if !current_input.is_empty() && current_pos < current_input.len().saturating_sub(1) { let new_pos = current_pos + 1; state.set_current_cursor_pos(new_pos); // Use trait setter *ideal_cursor_column = new_pos; } Ok("".to_string()) } "move_word_next" => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); // Clamp to last valid character index in read-only let final_pos = new_pos.min(current_input.len().saturating_sub(1)); state.set_current_cursor_pos(final_pos); // Use trait setter *ideal_cursor_column = final_pos; } Ok("".to_string()) } "move_word_end" => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = find_word_end(current_input, state.current_cursor_pos()); // Clamp to last valid character index in read-only let final_pos = new_pos.min(current_input.len().saturating_sub(1)); state.set_current_cursor_pos(final_pos); // Use trait setter *ideal_cursor_column = final_pos; } Ok("".to_string()) } "move_word_prev" => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = find_prev_word_start( current_input, state.current_cursor_pos(), ); state.set_current_cursor_pos(new_pos); // Use trait setter *ideal_cursor_column = new_pos; } Ok("".to_string()) } "move_word_end_prev" => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = find_prev_word_end( current_input, state.current_cursor_pos(), ); state.set_current_cursor_pos(new_pos); // Use trait setter *ideal_cursor_column = new_pos; } Ok("Moved to previous word end".to_string()) } "move_line_start" => { state.set_current_cursor_pos(0); // Use trait setter *ideal_cursor_column = 0; Ok("".to_string()) } "move_line_end" => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = current_input.len().saturating_sub(1); state.set_current_cursor_pos(new_pos); // Use trait setter *ideal_cursor_column = new_pos; } else { state.set_current_cursor_pos(0); // Handle empty input case *ideal_cursor_column = 0; } Ok("".to_string()) } "move_first_line" => { // Field change is handled outside based on context // Just set cursor position based on ideal column let current_input = state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len().saturating_sub(1) } else { 0 }; state.set_current_cursor_pos( (*ideal_cursor_column).min(max_cursor_pos), ); Ok("Moved to first line".to_string()) // Message might be inaccurate now } "move_last_line" => { // Field change is handled outside based on context // Just set cursor position based on ideal column let current_input = state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len().saturating_sub(1) } else { 0 }; state.set_current_cursor_pos( (*ideal_cursor_column).min(max_cursor_pos), ); Ok("Moved to last line".to_string()) // Message might be inaccurate now } _ => Ok(format!("Unknown read-only action: {}", action)), } } // Helper functions remain unchanged as they operate on &str 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; // Handle edge case where current_pos might be out of bounds after edits if pos >= chars.len() { return chars.len(); } let initial_type = get_char_type(chars[pos]); // Move past characters of the same type while pos < chars.len() && get_char_type(chars[pos]) == initial_type { pos += 1; } // Move past whitespace 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; } // Handle edge case where current_pos might be out of bounds let mut pos = current_pos.min(chars.len().saturating_sub(1)); // If starting on whitespace, move to the next non-whitespace if get_char_type(chars[pos]) == CharType::Whitespace { while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == CharType::Whitespace { pos += 1; } // If we are still on whitespace (meaning end of string or only whitespace left), return current pos if pos + 1 >= chars.len() || get_char_type(chars[pos + 1]) == CharType::Whitespace { return pos; } // Move to the start of the next word pos += 1; } // Now we are on a non-whitespace character 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); // Move past whitespace while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; } // Now on a non-whitespace or at the beginning if get_char_type(chars[pos]) != CharType::Whitespace { let word_type = get_char_type(chars[pos]); // Move to the beginning of the word while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; } } // If pos is 0 and it's whitespace, keep it at 0. Otherwise, it's the start of the word. if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { 0 } else { 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 { // Need at least 2 chars to find a *previous* word end return 0; } let mut pos = current_pos.saturating_sub(1); // Start looking one char back // Skip trailing whitespace from the current position while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; } // Now we are at the end of the word the cursor was in/after, or at the start if pos == 0 { return 0; // Reached the beginning } // Skip the word itself let word_type = get_char_type(chars[pos]); while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; } // Skip whitespace before that word while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { pos -= 1; } // Now pos is at the beginning of the word *or* the end of the *previous* word. // If we moved back, pos-1 is the index we want. if pos > 0 { pos - 1 } else { 0 // We were at the first word } }