diff --git a/client/src/modes/canvas/read_only.rs b/client/src/modes/canvas/read_only.rs index 8161abb..4ce9286 100644 --- a/client/src/modes/canvas/read_only.rs +++ b/client/src/modes/canvas/read_only.rs @@ -1,11 +1,12 @@ // src/modes/canvas/read_only.rs -use crossterm::event::{KeyEvent}; use crate::config::binds::config::Config; -use crate::state::pages::form::FormState; -use crate::state::pages::auth::AuthState; -use crate::services::grpc_client::GrpcClient; 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 { @@ -18,12 +19,12 @@ pub async fn handle_read_only_event( app_state: &crate::state::state::AppState, key: KeyEvent, config: &Config, - form_state: &mut FormState, - auth_state: &mut AuthState, + 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, - total_count: u64, - grpc_client: &mut GrpcClient, + 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, @@ -32,17 +33,37 @@ pub async fn handle_read_only_event( 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) { - 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; + // 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())); } @@ -52,9 +73,13 @@ pub async fn handle_read_only_event( 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) { - let result = if (action == "previous_entry" || action == "next_entry" || - action == "move_up" || action == "move_down") && app_state.ui.show_form { + 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, @@ -62,24 +87,52 @@ pub async fn handle_read_only_event( current_position, total_count, ideal_cursor_column, - ).await? - } else if (action == "move_up" || action == "move_down") && app_state.ui.show_login { + ) + .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 { - execute_action( + ) + .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, - ideal_cursor_column, - key_sequence_tracker, - command_message, + grpc_client, // Might not be needed for simple up/down current_position, total_count, - grpc_client, - ).await? + 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)); @@ -92,8 +145,14 @@ pub async fn handle_read_only_event( // 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) { - let result = if action == "previous_entry" && app_state.ui.show_form { + 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, @@ -101,18 +160,29 @@ pub async fn handle_read_only_event( current_position, total_count, ideal_cursor_column, - ).await? + ) + .await? } else { - execute_action( - action, - form_state, - ideal_cursor_column, - key_sequence_tracker, - command_message, - current_position, - total_count, - grpc_client, - ).await? + // 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)); @@ -122,8 +192,14 @@ 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_read_only_action_for_key(key.code, key.modifiers) { - let result = if action == "previous_entry" && app_state.ui.show_form { + 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, @@ -131,18 +207,29 @@ pub async fn handle_read_only_event( current_position, total_count, ideal_cursor_column, - ).await? + ) + .await? } else { - execute_action( - action, - form_state, - ideal_cursor_column, - key_sequence_tracker, - command_message, - current_position, - total_count, - grpc_client, - ).await? + // 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)); } @@ -151,7 +238,10 @@ 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.read_only.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); @@ -160,128 +250,155 @@ pub async fn handle_read_only_event( Ok((false, command_message.clone())) } -async fn execute_action( +// Make this function generic over CanvasState +async fn execute_action( action: &str, - form_state: &mut FormState, + state: &mut S, // Use generic state ideal_cursor_column: &mut usize, - key_sequence_tracker: &mut KeySequenceTracker, - command_message: &mut String, - current_position: &mut u64, - total_count: u64, - grpc_client: &mut GrpcClient, + 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" => { - // This will only be called when no component is active key_sequence_tracker.reset(); - Ok(format!("Navigation prev/next only available in form mode")) + Ok(format!( + "Action '{}' should be handled by context-specific logic", + action + )) } + // These actions are handled outside now based on context "move_up" | "move_down" => { - // This will only be called when no component is active key_sequence_tracker.reset(); - Ok(format!("Navigation up/down only available in form mode")) + 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 = form_state.current_cursor_pos; - form_state.current_cursor_pos = current_pos.saturating_sub(1); - *ideal_cursor_column = form_state.current_cursor_pos; + 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 = form_state.get_current_input(); - let current_pos = form_state.current_cursor_pos; - 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; + 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 = form_state.get_current_input(); + let current_input = state.get_current_input(); if !current_input.is_empty() { - let new_pos = 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)); - *ideal_cursor_column = form_state.current_cursor_pos; + 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 = form_state.get_current_input(); + let current_input = state.get_current_input(); if !current_input.is_empty() { - let new_pos = find_word_end(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); - *ideal_cursor_column = form_state.current_cursor_pos; + 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 = form_state.get_current_input(); + let current_input = state.get_current_input(); if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos; - *ideal_cursor_column = form_state.current_cursor_pos; + 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 = form_state.get_current_input(); + let current_input = state.get_current_input(); if !current_input.is_empty() { - let new_pos = find_prev_word_end(current_input, form_state.current_cursor_pos); - form_state.current_cursor_pos = new_pos; - *ideal_cursor_column = form_state.current_cursor_pos; + 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" => { - form_state.current_cursor_pos = 0; - *ideal_cursor_column = form_state.current_cursor_pos; + state.set_current_cursor_pos(0); // Use trait setter + *ideal_cursor_column = 0; Ok("".to_string()) } "move_line_end" => { - let current_input = form_state.get_current_input(); + let current_input = state.get_current_input(); if !current_input.is_empty() { - form_state.current_cursor_pos = current_input.len() - 1; - *ideal_cursor_column = form_state.current_cursor_pos; + 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" => { - // Change field first - form_state.current_field = 0; - - // Get current input AFTER changing field - let current_input = form_state.get_current_input(); + // 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() - 1 + current_input.len().saturating_sub(1) } else { - current_input.len() + 0 }; - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - Ok("Moved to first line".to_string()) + 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" => { - // 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(); + // 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() - 1 + current_input.len().saturating_sub(1) } else { - current_input.len() + 0 }; - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - Ok("Moved to last line".to_string()) + 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 action: {}", action)), + _ => 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 @@ -299,11 +416,18 @@ fn find_next_word_start(text: &str, current_pos: usize) -> usize { } 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; } @@ -311,36 +435,29 @@ fn find_next_word_start(text: &str, current_pos: usize) -> usize { 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 current_pos >= chars.len() - 1 { - return chars.len() - 1; - } - - let mut pos = current_pos; - + // If starting on whitespace, move to the next non-whitespace if get_char_type(chars[pos]) == CharType::Whitespace { - while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == 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 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; } - if pos >= chars.len() { - return chars.len() - 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; @@ -349,6 +466,7 @@ fn find_word_end(text: &str, current_pos: usize) -> usize { 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 { @@ -357,56 +475,64 @@ fn find_prev_word_start(text: &str, current_pos: usize) -> usize { 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; } } - pos + // 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 { + 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); + 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; } - 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; - } - } + // 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 } - pos + // 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 + } } +