diff --git a/client/src/functions/modes/edit.rs b/client/src/functions/modes/edit.rs index dd8db60..3196927 100644 --- a/client/src/functions/modes/edit.rs +++ b/client/src/functions/modes/edit.rs @@ -2,3 +2,4 @@ pub mod form_e; pub mod auth_e; +pub mod add_table_e; diff --git a/client/src/functions/modes/edit/add_table_e.rs b/client/src/functions/modes/edit/add_table_e.rs new file mode 100644 index 0000000..855e6aa --- /dev/null +++ b/client/src/functions/modes/edit/add_table_e.rs @@ -0,0 +1,343 @@ +// src/functions/modes/edit/add_table_e.rs +use crate::state::pages::add_table::AddTableState; +use crate::state::pages::canvas_state::CanvasState; // Use trait +use crossterm::event::{KeyCode, KeyEvent}; +use std::error::Error; + +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + +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(); + let len = chars.len(); + if len == 0 || current_pos >= len { + return len; + } + + let mut pos = current_pos; + let initial_type = get_char_type(chars[pos]); + + while pos < len && get_char_type(chars[pos]) == initial_type { + pos += 1; + } + + while pos < 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(); + let len = chars.len(); + if len == 0 { + return 0; + } + + let mut pos = current_pos.min(len - 1); + + if get_char_type(chars[pos]) == CharType::Whitespace { + pos = find_next_word_start(text, pos); + } + + if pos >= len { + return len.saturating_sub(1); + } + + let word_type = get_char_type(chars[pos]); + while pos < len && get_char_type(chars[pos]) == word_type { + pos += 1; + } + + pos.saturating_sub(1).min(len.saturating_sub(1)) +} + +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 pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { + return 0; + } + + 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(); + let len = chars.len(); + if len == 0 || 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 pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { + return 0; + } + if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace { + return 0; + } + + 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 + } else { + 0 + } +} + +/// Executes edit actions for the AddTable view canvas. +pub async fn execute_edit_action( + action: &str, + key: KeyEvent, // Needed for insert_char + state: &mut AddTableState, + ideal_cursor_column: &mut usize, + // Add other params like grpc_client if needed for future actions (e.g., validation) +) -> Result> { + // Use the CanvasState trait methods implemented for AddTableState + match action { + "insert_char" => { + if let KeyCode::Char(c) = key.code { + let cursor_pos = state.current_cursor_pos(); + let field_value = 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(); + state.set_current_cursor_pos(cursor_pos + 1); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = state.current_cursor_pos(); + } + } else { + return Ok("Error: insert_char called without a char key.".to_string()); + } + Ok("".to_string()) // No message needed for char insertion + } + "delete_char_backward" => { + if state.current_cursor_pos() > 0 { + let cursor_pos = state.current_cursor_pos(); + let field_value = state.get_current_input_mut(); + let mut chars: Vec = field_value.chars().collect(); + if cursor_pos <= chars.len() { + chars.remove(cursor_pos - 1); + *field_value = chars.into_iter().collect(); + let new_pos = cursor_pos - 1; + state.set_current_cursor_pos(new_pos); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = new_pos; + } + } + Ok("".to_string()) + } + "delete_char_forward" => { + let cursor_pos = state.current_cursor_pos(); + let field_value = state.get_current_input_mut(); + let mut chars: Vec = field_value.chars().collect(); + if cursor_pos < chars.len() { + chars.remove(cursor_pos); + *field_value = chars.into_iter().collect(); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos; + } + Ok("".to_string()) + } + "next_field" => { + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let current_field = state.current_field(); + let new_field = (current_field + 1) % num_fields; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + } + Ok("".to_string()) + } + "prev_field" => { + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let current_field = state.current_field(); + let new_field = if current_field == 0 { + num_fields - 1 + } else { + current_field - 1 + }; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + } + Ok("".to_string()) + } + "move_left" => { + let new_pos = state.current_cursor_pos().saturating_sub(1); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + "move_right" => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + if current_pos < current_input.len() { + let new_pos = current_pos + 1; + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok("".to_string()) + } + "move_up" => { + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let current_field = state.current_field(); + if current_field > 0 { + let new_field = current_field - 1; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + } + } + Ok("".to_string()) + } + "move_down" => { + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let current_field = state.current_field(); + let last_field_index = num_fields - 1; + if current_field < last_field_index { + let new_field = current_field + 1; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + } + } + Ok("".to_string()) + } + "move_line_start" => { + state.set_current_cursor_pos(0); + *ideal_cursor_column = 0; + Ok("".to_string()) + } + "move_line_end" => { + let current_input = state.get_current_input(); + let new_pos = current_input.len(); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + "move_first_line" => { + if AddTableState::INPUT_FIELD_COUNT > 0 { + state.set_current_field(0); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + } + Ok("".to_string()) + } + "move_last_line" => { + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let new_field = num_fields - 1; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_pos = current_input.len(); + state.set_current_cursor_pos((*ideal_cursor_column).min(max_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()); + let final_pos = new_pos.min(current_input.len()); + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + } + Ok("".to_string()) + } + "move_word_end" => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let current_pos = state.current_cursor_pos(); + let new_pos = find_word_end(current_input, current_pos); + + let final_pos = if new_pos == current_pos { + find_word_end(current_input, new_pos + 1) + } else { + new_pos + }; + + let max_valid_index = current_input.len().saturating_sub(1); + let clamped_pos = final_pos.min(max_valid_index); + state.set_current_cursor_pos(clamped_pos); + *ideal_cursor_column = clamped_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); + *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); + *ideal_cursor_column = new_pos; + } + Ok("".to_string()) + } + // Actions handled by main event loop (mode changes, save, revert) + "exit_edit_mode" | "save" | "revert" => { + Ok("Action handled by main loop".to_string()) + } + _ => Ok(format!("Unknown or unhandled edit action: {}", action)), + } +} diff --git a/client/src/functions/modes/navigation/add_table_nav.rs b/client/src/functions/modes/navigation/add_table_nav.rs index e4d3609..a0e1cbd 100644 --- a/client/src/functions/modes/navigation/add_table_nav.rs +++ b/client/src/functions/modes/navigation/add_table_nav.rs @@ -5,101 +5,120 @@ use crate::state::{ pages::add_table::{AddTableFocus, AddTableState}, }; use crossterm::event::{KeyEvent}; -use ratatui::widgets::TableState; // Import TableState +use ratatui::widgets::TableState; /// Handles navigation events specifically for the Add Table view. /// Returns true if the event was handled, false otherwise. pub fn handle_add_table_navigation( key: KeyEvent, config: &Config, - _app_state: &AppState, // Keep for potential future use (e.g., checking permissions) + _app_state: &AppState, add_table_state: &mut AddTableState, command_message: &mut String, ) -> bool { let action = config.get_general_action(key.code, key.modifiers); let current_focus = add_table_state.current_focus; let mut handled = true; // Assume handled unless logic determines otherwise + let mut new_focus = current_focus; // Initialize new_focus + + // Define focus groups for horizontal navigation + let is_left_pane_focus = matches!(current_focus, + AddTableFocus::ColumnsTable | AddTableFocus::IndexesTable | AddTableFocus::LinksTable + ); + let is_right_pane_focus = matches!(current_focus, + AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType | + AddTableFocus::AddColumnButton | AddTableFocus::SaveButton | AddTableFocus::CancelButton + ); match action.as_deref() { // --- Vertical Navigation (Up/Down) --- Some("move_up") => { - let mut new_focus = current_focus; // Start with current focus match current_focus { - AddTableFocus::InputTableName => new_focus = AddTableFocus::CancelButton, // Wrap top + AddTableFocus::InputTableName => new_focus = AddTableFocus::CancelButton, // Wrap top (right pane) AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputTableName, AddTableFocus::InputColumnType => new_focus = AddTableFocus::InputColumnName, AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType, - AddTableFocus::ColumnsTable => { + AddTableFocus::ColumnsTable => { // Left pane navigation if !navigate_table_up(&mut add_table_state.column_table_state, add_table_state.columns.len()) { - new_focus = AddTableFocus::AddColumnButton; // Move focus up if at table top + // If at top of columns, potentially wrap to bottom of left pane (LinksTable) or stay? Let's stay for now. + // Or maybe move to AddColumnButton? Let's try moving up from right pane instead. + new_focus = AddTableFocus::AddColumnButton; // Tentative: move focus up from right pane } - // Keep focus on table while navigating within it } AddTableFocus::IndexesTable => { if !navigate_table_up(&mut add_table_state.index_table_state, add_table_state.indexes.len()) { - new_focus = AddTableFocus::ColumnsTable; // Move focus up + new_focus = AddTableFocus::ColumnsTable; } } AddTableFocus::LinksTable => { if !navigate_table_up(&mut add_table_state.link_table_state, add_table_state.links.len()) { - new_focus = AddTableFocus::IndexesTable; // Move focus up + new_focus = AddTableFocus::IndexesTable; } } - AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable, + AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable, // Move up to left pane bottom AddTableFocus::CancelButton => new_focus = AddTableFocus::SaveButton, } - add_table_state.current_focus = new_focus; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); } Some("move_down") => { - let mut new_focus = current_focus; // Start with current focus match current_focus { AddTableFocus::InputTableName => new_focus = AddTableFocus::InputColumnName, AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputColumnType, AddTableFocus::InputColumnType => new_focus = AddTableFocus::AddColumnButton, - AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable, - AddTableFocus::ColumnsTable => { + AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable, // Move down to left pane top + AddTableFocus::ColumnsTable => { // Left pane navigation if !navigate_table_down(&mut add_table_state.column_table_state, add_table_state.columns.len()) { - new_focus = AddTableFocus::IndexesTable; // Move focus down if at table bottom + new_focus = AddTableFocus::IndexesTable; // Move to next left pane item } - // Keep focus on table while navigating within it } AddTableFocus::IndexesTable => { if !navigate_table_down(&mut add_table_state.index_table_state, add_table_state.indexes.len()) { - new_focus = AddTableFocus::LinksTable; // Move focus down + new_focus = AddTableFocus::LinksTable; } } AddTableFocus::LinksTable => { if !navigate_table_down(&mut add_table_state.link_table_state, add_table_state.links.len()) { - new_focus = AddTableFocus::SaveButton; // Move focus down + new_focus = AddTableFocus::SaveButton; // Move down to right pane bottom } } AddTableFocus::SaveButton => new_focus = AddTableFocus::CancelButton, - AddTableFocus::CancelButton => new_focus = AddTableFocus::InputTableName, // Wrap bottom + AddTableFocus::CancelButton => new_focus = AddTableFocus::InputTableName, // Wrap bottom (right pane) } - add_table_state.current_focus = new_focus; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); } // --- Horizontal Navigation (Left/Right) --- - Some("next_option") => { // 'l' or Right - add_table_state.current_focus = match current_focus { - AddTableFocus::SaveButton => AddTableFocus::CancelButton, - _ => current_focus, // No change for others yet - }; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); + Some("next_option") => { // 'l' or Right: Move from Left Pane to Right Pane + if is_left_pane_focus { + new_focus = match current_focus { + // Map left pane items to corresponding right pane items (approximate vertical alignment) + AddTableFocus::ColumnsTable => AddTableFocus::InputTableName, + AddTableFocus::IndexesTable => AddTableFocus::InputColumnName, // Or AddColumnButton? + AddTableFocus::LinksTable => AddTableFocus::SaveButton, + _ => current_focus, // Should not happen based on is_left_pane_focus + }; + } else if is_right_pane_focus { + // If already in right pane, maybe wrap Save -> Cancel or stay? Let's handle Save->Cancel only. + if current_focus == AddTableFocus::SaveButton { + new_focus = AddTableFocus::CancelButton; + } + } } - Some("previous_option") => { // 'h' or Left - add_table_state.current_focus = match current_focus { - AddTableFocus::CancelButton => AddTableFocus::SaveButton, - _ => current_focus, // No change for others yet - }; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); + Some("previous_option") => { // 'h' or Left: Move from Right Pane to Left Pane + if is_right_pane_focus { + new_focus = match current_focus { + // Map right pane items back to left pane items (approximate vertical alignment) + AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType | AddTableFocus::AddColumnButton => AddTableFocus::ColumnsTable, // Go to top of left pane + AddTableFocus::SaveButton | AddTableFocus::CancelButton => AddTableFocus::LinksTable, // Go to bottom of left pane + _ => current_focus, // Should not happen + }; + } else if is_left_pane_focus { + // If already in left pane, pressing 'h' could wrap to Cancel button? + new_focus = AddTableFocus::CancelButton; // Wrap left-to-right bottom + } } - // --- Tab / Shift+Tab Navigation --- + // --- Tab / Shift+Tab Navigation (Keep as vertical cycle) --- Some("next_field") => { // Tab - add_table_state.current_focus = match current_focus { + new_focus = match current_focus { AddTableFocus::InputTableName => AddTableFocus::InputColumnName, AddTableFocus::InputColumnName => AddTableFocus::InputColumnType, AddTableFocus::InputColumnType => AddTableFocus::AddColumnButton, @@ -110,10 +129,9 @@ pub fn handle_add_table_navigation( AddTableFocus::SaveButton => AddTableFocus::CancelButton, AddTableFocus::CancelButton => AddTableFocus::InputTableName, // Wrap }; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); } Some("prev_field") => { // Shift+Tab - add_table_state.current_focus = match current_focus { + new_focus = match current_focus { AddTableFocus::InputTableName => AddTableFocus::CancelButton, // Wrap AddTableFocus::InputColumnName => AddTableFocus::InputTableName, AddTableFocus::InputColumnType => AddTableFocus::InputColumnName, @@ -124,7 +142,6 @@ pub fn handle_add_table_navigation( AddTableFocus::SaveButton => AddTableFocus::LinksTable, AddTableFocus::CancelButton => AddTableFocus::SaveButton, }; - *command_message = format!("Focus set to {:?}", add_table_state.current_focus); } // --- Selection --- @@ -132,70 +149,54 @@ pub fn handle_add_table_navigation( match current_focus { AddTableFocus::AddColumnButton => { *command_message = "Action: Add Column (Not Implemented)".to_string(); - // TODO: Implement logic to add column based on inputs - // Clear input fields, add to columns list, mark unsaved changes - // add_table_state.add_column(); // Example method call + // TODO: Implement logic } AddTableFocus::SaveButton => { *command_message = "Action: Save Table (Not Implemented)".to_string(); - // TODO: Implement logic to save table (e.g., call API) - // Mark changes as saved - // add_table_state.save_table(); // Example method call + // TODO: Implement logic } AddTableFocus::CancelButton => { *command_message = "Action: Cancel Add Table".to_string(); - // TODO: Implement logic to navigate back (e.g., update AppView history) - // Maybe show a confirmation dialog if there are unsaved changes - // buffer_state.go_back(); // Example call + // TODO: Implement logic } - // Selecting input fields usually means entering Edit mode (handled elsewhere) - // Selecting tables might mean focusing on them for editing/deletion (TODO) AddTableFocus::ColumnsTable => { if let Some(index) = add_table_state.column_table_state.selected() { *command_message = format!("Selected column index {}", index); - // TODO: Add logic for editing/deleting selected column - } else { - *command_message = "No column selected".to_string(); - } + } else { *command_message = "No column selected".to_string(); } } AddTableFocus::IndexesTable => { if let Some(index) = add_table_state.index_table_state.selected() { *command_message = format!("Selected index index {}", index); - // TODO: Add logic for editing/deleting selected index - } else { - *command_message = "No index selected".to_string(); - } + } else { *command_message = "No index selected".to_string(); } } AddTableFocus::LinksTable => { if let Some(index) = add_table_state.link_table_state.selected() { *command_message = format!("Selected link index {}", index); - // TODO: Add logic for editing/deleting selected link - } else { - *command_message = "No link selected".to_string(); - } + } else { *command_message = "No link selected".to_string(); } } - _ => { - // For InputTableName, InputColumnName, InputColumnType, - // the main event loop should handle 'select' by potentially - // switching to Edit mode if not already in it. - // We don't need specific logic here for that. + _ => { // Input fields *command_message = format!("Select on {:?}", current_focus); handled = false; // Let main loop handle edit mode toggle maybe } } + // Keep handled = true for select actions unless specifically set to false } - // --- Other General Keys (Ignore for add_table nav) --- + // --- Other General Keys --- Some("toggle_sidebar") | Some("toggle_buffer_list") => { - handled = false; // Let global handler manage these + handled = false; } // --- No matching action --- - _ => handled = false, // Event not handled by add_table navigation + _ => handled = false, } - // If focus changed TO a table, select the first row if nothing is selected - if handled && current_focus != add_table_state.current_focus { + // Update focus state if it changed and was handled + if handled && current_focus != new_focus { + add_table_state.current_focus = new_focus; + *command_message = format!("Focus set to {:?}", add_table_state.current_focus); + + // Select first item when focusing a table match add_table_state.current_focus { AddTableFocus::ColumnsTable if add_table_state.column_table_state.selected().is_none() && !add_table_state.columns.is_empty() => { add_table_state.column_table_state.select(Some(0)); @@ -206,10 +207,14 @@ pub fn handle_add_table_navigation( AddTableFocus::LinksTable if add_table_state.link_table_state.selected().is_none() && !add_table_state.links.is_empty() => { add_table_state.link_table_state.select(Some(0)); } - _ => {} // No action needed for other focus states + _ => {} } + } else if !handled { + // If not handled by this specific navigation, clear the message potentially set by get_general_action + // command_message.clear(); // Optional: depends if you want default messages } + handled } @@ -217,20 +222,19 @@ pub fn handle_add_table_navigation( // Helper function for navigating up within a table state // Returns true if navigation happened within the table, false if it reached the top fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool { - if item_count == 0 { return false; } // Cannot navigate empty table + if item_count == 0 { return false; } let current_selection = table_state.selected(); match current_selection { Some(index) => { if index > 0 { table_state.select(Some(index - 1)); - true // Moved up within table + true } else { false // Was at the top } } None => { - // If nothing selected, moving up could select the last item - table_state.select(Some(item_count - 1)); + table_state.select(Some(item_count - 1)); // Select last item true } } @@ -239,20 +243,19 @@ fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool { // Helper function for navigating down within a table state // Returns true if navigation happened within the table, false if it reached the bottom fn navigate_table_down(table_state: &mut TableState, item_count: usize) -> bool { - if item_count == 0 { return false; } // Cannot navigate empty table + if item_count == 0 { return false; } let current_selection = table_state.selected(); match current_selection { Some(index) => { if index < item_count - 1 { table_state.select(Some(index + 1)); - true // Moved down within table + true } else { false // Was at the bottom } } None => { - // If nothing selected, moving down could select the first item - table_state.select(Some(0)); + table_state.select(Some(0)); // Select first item true } } diff --git a/client/src/functions/modes/read_only.rs b/client/src/functions/modes/read_only.rs index 04920b4..038efcf 100644 --- a/client/src/functions/modes/read_only.rs +++ b/client/src/functions/modes/read_only.rs @@ -2,3 +2,4 @@ pub mod auth_ro; pub mod form_ro; +pub mod add_table_ro; diff --git a/client/src/functions/modes/read_only/add_table_ro.rs b/client/src/functions/modes/read_only/add_table_ro.rs new file mode 100644 index 0000000..af2b252 --- /dev/null +++ b/client/src/functions/modes/read_only/add_table_ro.rs @@ -0,0 +1,229 @@ +// src/functions/modes/read_only/add_table_ro.rs +use crate::config::binds::key_sequences::KeySequenceTracker; +use crate::state::pages::add_table::AddTableState; +use crate::state::pages::canvas_state::CanvasState; // Use trait for common actions +use crate::state::app::state::AppState; +use std::error::Error; + +// Re-use word navigation helpers if they are public or move them to a common module +// For now, duplicating them here for simplicity. Consider refactoring later. +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + +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(); + let len = chars.len(); + if len == 0 || current_pos >= len { return len; } + let mut pos = current_pos; + let initial_type = get_char_type(chars[pos]); + while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; } + while pos < 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(); + let len = chars.len(); + if len == 0 { return 0; } + let mut pos = current_pos.min(len - 1); + if get_char_type(chars[pos]) == CharType::Whitespace { + pos = find_next_word_start(text, pos); + } + if pos >= len { return len.saturating_sub(1); } + let word_type = get_char_type(chars[pos]); + while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; } + pos.saturating_sub(1).min(len.saturating_sub(1)) +} + +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 pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; } + let word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; } + pos +} + +// Note: find_prev_word_end might need adjustments based on desired behavior. +// This version finds the end of the word *before* the previous word start. +fn find_prev_word_end(text: &str, current_pos: usize) -> usize { + let prev_start = find_prev_word_start(text, current_pos); + if prev_start == 0 { return 0; } + // Find the end of the word that starts at prev_start - 1 + find_word_end(text, prev_start.saturating_sub(1)) +} + + +/// Executes read-only actions for the AddTable view canvas. +pub async fn execute_action( + action: &str, + app_state: &mut AppState, // Needed for focus_outside_canvas + state: &mut AddTableState, + ideal_cursor_column: &mut usize, + key_sequence_tracker: &mut KeySequenceTracker, + command_message: &mut String, // Keep for potential messages +) -> Result> { + // Use the CanvasState trait methods implemented for AddTableState + match action { + "move_up" => { + key_sequence_tracker.reset(); + let num_fields = AddTableState::INPUT_FIELD_COUNT; // Use the constant + if num_fields == 0 { return Ok("No fields.".to_string()); } + let current_field = state.current_field(); + if current_field > 0 { + let new_field = current_field - 1; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_cursor_pos = current_input.len(); // Allow cursor at end + let new_pos = (*ideal_cursor_column).min(max_cursor_pos); + state.set_current_cursor_pos(new_pos); + } else { + // Optionally move focus outside canvas when moving up from the first field + // app_state.ui.focus_outside_canvas = true; + // return Ok("Focus moved above canvas".to_string()); + } + Ok("".to_string()) + } + "move_down" => { + key_sequence_tracker.reset(); + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields == 0 { return Ok("No fields.".to_string()); } + let current_field = state.current_field(); + let last_field_index = num_fields - 1; + + if current_field < last_field_index { + let new_field = current_field + 1; + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let max_cursor_pos = current_input.len(); // Allow cursor at end + let new_pos = (*ideal_cursor_column).min(max_cursor_pos); + state.set_current_cursor_pos(new_pos); + } else { + // Move focus outside canvas when moving down from the last field + app_state.ui.focus_outside_canvas = true; + // Set focus to the first element outside canvas (AddColumnButton) + state.current_focus = crate::state::pages::add_table::AddTableFocus::AddColumnButton; + key_sequence_tracker.reset(); + return Ok("Focus moved below canvas".to_string()); + } + Ok("".to_string()) + } + "move_first_line" => { + key_sequence_tracker.reset(); + if AddTableState::INPUT_FIELD_COUNT > 0 { + state.set_current_field(0); + let current_input = state.get_current_input(); + let max_cursor_pos = current_input.len(); + let new_pos = (*ideal_cursor_column).min(max_cursor_pos); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; // Update ideal column + } + Ok("".to_string()) + } + "move_last_line" => { + key_sequence_tracker.reset(); + let num_fields = AddTableState::INPUT_FIELD_COUNT; + if num_fields > 0 { + let last_field_index = num_fields - 1; + state.set_current_field(last_field_index); + let current_input = state.get_current_input(); + let max_cursor_pos = current_input.len(); + let new_pos = (*ideal_cursor_column).min(max_cursor_pos); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; // Update ideal column + } + 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); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + "move_right" => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + // Allow moving cursor one position past the end + if current_pos < current_input.len() { + let new_pos = current_pos + 1; + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok("".to_string()) + } + "move_word_next" => { + let current_input = state.get_current_input(); + let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); + let final_pos = new_pos.min(current_input.len()); // Allow cursor at end + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + Ok("".to_string()) + } + "move_word_end" => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + let new_pos = find_word_end(current_input, current_pos); + // If find_word_end returns current_pos, try starting search from next char + let final_pos = if new_pos == current_pos && current_pos < current_input.len() { + find_word_end(current_input, current_pos + 1) + } else { + new_pos + }; + let max_valid_index = current_input.len(); // Allow cursor at end + let clamped_pos = final_pos.min(max_valid_index); + state.set_current_cursor_pos(clamped_pos); + *ideal_cursor_column = clamped_pos; + Ok("".to_string()) + } + "move_word_prev" => { + let current_input = state.get_current_input(); + let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + "move_word_end_prev" => { + let current_input = state.get_current_input(); + let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + "move_line_start" => { + state.set_current_cursor_pos(0); + *ideal_cursor_column = 0; + Ok("".to_string()) + } + "move_line_end" => { + let current_input = state.get_current_input(); + let new_pos = current_input.len(); // Allow cursor at end + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok("".to_string()) + } + // Actions handled by main event loop (mode changes) + "enter_edit_mode_before" | "enter_edit_mode_after" | "enter_command_mode" | "exit_highlight_mode" => { + key_sequence_tracker.reset(); + Ok("Mode change handled by main loop".to_string()) + } + _ => { + key_sequence_tracker.reset(); + command_message.clear(); // Clear message for unhandled actions + Ok(format!("Unknown read-only action: {}", action)) + }, + } +} + diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 7a901d5..e26a0e3 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -1,13 +1,17 @@ // src/modes/canvas/edit.rs use crate::config::binds::config::Config; use crate::services::grpc_client::GrpcClient; -use crate::state::pages::{auth::{LoginState, RegisterState}, canvas_state::CanvasState}; +use crate::state::pages::{ + auth::{LoginState, RegisterState}, + canvas_state::CanvasState, +}; use crate::state::pages::form::FormState; +use crate::state::pages::add_table::AddTableState; // Added use crate::modes::handlers::event::EventOutcome; use crate::functions::modes::edit::{auth_e, form_e}; +use crate::functions::modes::edit::add_table_e; // Added use crate::state::app::state::AppState; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -// Removed duplicate/unused imports #[derive(Debug, Clone, PartialEq, Eq)] pub enum EditEventOutcome { @@ -21,210 +25,319 @@ pub async fn handle_edit_event( form_state: &mut FormState, login_state: &mut LoginState, register_state: &mut RegisterState, + add_table_state: &mut AddTableState, // Added ideal_cursor_column: &mut usize, - // command_message: &mut String, // Removed as messages are returned current_position: &mut u64, total_count: u64, grpc_client: &mut GrpcClient, app_state: &AppState, ) -> Result> { - - // Global command mode check + // Global command mode check (should ideally be handled before calling this function) if let Some("enter_command_mode") = config.get_action_for_key_in_mode( &config.keybindings.global, key.code, - key.modifiers + key.modifiers, ) { - return Ok(EditEventOutcome::Message("Switching to Command Mode...".to_string())); + // This mode change should likely be handled in event.rs + // Returning a message here might prevent the mode switch. + // Consider if this check is necessary here. + return Ok(EditEventOutcome::Message( + "Command mode entry handled globally.".to_string(), + )); } - // Common actions (save/revert) if let Some(action) = config.get_action_for_key_in_mode( &config.keybindings.common, key.code, - key.modifiers - ) { + key.modifiers, + ).as_deref() { if matches!(action, "save" | "revert") { - // Ensure all branches result in Result before the final Ok(...) wrap let message_string: String = if app_state.ui.show_login { auth_e::execute_common_action( action, login_state, grpc_client, current_position, - total_count - ).await? // Results in String on success + total_count, + ) + .await? } else if app_state.ui.show_register { + // Keeping this block as requested auth_e::execute_common_action( action, register_state, grpc_client, current_position, - total_count - ).await? // Results in String on success - } else { + total_count, + ) + .await? // Results in String on success + } else if app_state.ui.show_add_table { + // Placeholder - common actions for AddTable might be different + format!( + "Action '{}' not fully implemented for Add Table view here.", + action + ) + } else { // Assuming FormState otherwise let outcome = form_e::execute_common_action( action, form_state, grpc_client, current_position, - total_count - ).await?; // This returns EventOutcome on success - - // Extract the message string from the EventOutcome + total_count, + ) + .await?; match outcome { EventOutcome::Ok(msg) => msg, EventOutcome::DataSaved(_, msg) => msg, - _ => format!("Unexpected outcome from common action: {:?}", outcome), + _ => format!( + "Unexpected outcome from common action: {:?}", + outcome + ), } }; - // Wrap the resulting String message return Ok(EditEventOutcome::Message(message_string)); } } // Edit-specific actions - if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { - if action == "exit" { + if let Some(action) = + config.get_edit_action_for_key(key.code, key.modifiers) + .as_deref() { + if action == "exit_edit_mode" { + // Handle exiting suggestion mode in Register view first if app_state.ui.show_register && register_state.in_suggestion_mode { - // Call the action, get Result let msg = auth_e::execute_edit_action( - "exit_suggestion_mode", + "exit_suggestion_mode", // Specific action for suggestion exit + key, + register_state, + ideal_cursor_column, + grpc_client, // Pass necessary args if needed by auth_e + current_position, + total_count, + ) + .await?; + return Ok(EditEventOutcome::Message(msg)); + } else { + // Signal exit from Edit mode + return Ok(EditEventOutcome::ExitEditMode); + } + } + + // Special handling for role field suggestions (Register view only) + if app_state.ui.show_register && register_state.current_field() == 4 { + // Check if Tab was pressed to *enter* suggestion mode + if !register_state.in_suggestion_mode + && key.code == KeyCode::Tab + && key.modifiers == KeyModifiers::NONE + { + register_state.update_role_suggestions(); + if !register_state.role_suggestions.is_empty() { + register_state.in_suggestion_mode = true; + register_state.selected_suggestion_index = Some(0); // Select first suggestion + return Ok(EditEventOutcome::Message( + "Suggestions shown".to_string(), + )); + } else { + return Ok(EditEventOutcome::Message( + "No suggestions available".to_string(), + )); + } + } + // Handle suggestion navigation/selection if already in suggestion mode + if register_state.in_suggestion_mode + && matches!( + action, + "suggestion_down" + | "suggestion_up" + | "select_suggestion" + ) + { + let msg = auth_e::execute_edit_action( + action, // Pass the specific suggestion action key, register_state, ideal_cursor_column, grpc_client, current_position, total_count, - ).await?; // Results in String on success - // Wrap the String message + ) + .await?; return Ok(EditEventOutcome::Message(msg)); - } else { - // Signal exit - return Ok(EditEventOutcome::ExitEditMode); } } - // Special handling for role field suggestions + // Execute other edit actions based on the current view + let msg = if app_state.ui.show_login { + auth_e::execute_edit_action( + action, + key, + login_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + } else if app_state.ui.show_add_table { + add_table_e::execute_edit_action( + action, + key, + add_table_state, + ideal_cursor_column, + // Pass other necessary params if add_table_e needs them + ) + .await? + } else if app_state.ui.show_register { + auth_e::execute_edit_action( + action, + key, + register_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + } else { + // Assuming FormState otherwise + form_e::execute_edit_action( + action, + key, + form_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + }; + return Ok(EditEventOutcome::Message(msg)); + } + + // --- Character insertion --- + if let KeyCode::Char(c) = key.code { + // Exit suggestion mode in Register view if a character is typed + if app_state.ui.show_register && register_state.in_suggestion_mode { + register_state.in_suggestion_mode = false; + register_state.show_role_suggestions = false; + register_state.selected_suggestion_index = None; + } + + // Execute insert_char action based on the current view + let msg = if app_state.ui.show_login { + auth_e::execute_edit_action( + "insert_char", + key, // Pass the key event containing the char + login_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + } else if app_state.ui.show_add_table { + add_table_e::execute_edit_action( + "insert_char", + key, + add_table_state, + ideal_cursor_column, + ) + .await? + } else if app_state.ui.show_register { + auth_e::execute_edit_action( + "insert_char", + key, + register_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + } else { + // Assuming FormState otherwise + form_e::execute_edit_action( + "insert_char", + key, + form_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + }; + // Update role suggestions after insertion if needed (Register view) if app_state.ui.show_register && register_state.current_field() == 4 { - if !register_state.in_suggestion_mode && key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE { - register_state.update_role_suggestions(); - if !register_state.role_suggestions.is_empty() { - register_state.in_suggestion_mode = true; - register_state.selected_suggestion_index = Some(0); - return Ok(EditEventOutcome::Message("Suggestions shown".to_string())); - } else { // Added else here for clarity - return Ok(EditEventOutcome::Message("No suggestions available".to_string())); - } - } + register_state.update_role_suggestions(); } - // Execute other edit actions - let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - action, - key, - login_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - } else if app_state.ui.show_register { - auth_e::execute_edit_action( - action, - key, - register_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - } else { - form_e::execute_edit_action( - action, - key, - form_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - }; - // Wrap the resulting String message return Ok(EditEventOutcome::Message(msg)); } - // Character insertion - if let KeyCode::Char(_) = key.code { - if app_state.ui.show_register && register_state.in_suggestion_mode { - register_state.in_suggestion_mode = false; - register_state.show_role_suggestions = false; - register_state.selected_suggestion_index = None; - } - - // Execute insert_char action - let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - "insert_char", - key, - login_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - } else if app_state.ui.show_register { - auth_e::execute_edit_action( - "insert_char", - key, - register_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - } else { - form_e::execute_edit_action( - "insert_char", - key, - form_state, - ideal_cursor_column, - grpc_client, - current_position, - total_count - ).await? // Results in String - }; - // Wrap the resulting String message - return Ok(EditEventOutcome::Message(msg)); - } - - // Handle Backspace/Delete + // --- Handle Backspace/Delete --- if matches!(key.code, KeyCode::Backspace | KeyCode::Delete) { + // Exit suggestion mode in Register view if app_state.ui.show_register && register_state.in_suggestion_mode { register_state.in_suggestion_mode = false; register_state.show_role_suggestions = false; register_state.selected_suggestion_index = None; } - let action_str = if key.code == KeyCode::Backspace { "backspace" } else { "delete_char" }; - // Ensure both branches result in a String *before* wrapping - let result_msg: String = if app_state.ui.show_register { - auth_e::execute_edit_action( + let action_str = if key.code == KeyCode::Backspace { + "delete_char_backward" + } else { + "delete_char_forward" + }; + + // Execute delete action based on the current view + let result_msg: String = if app_state.ui.show_login { + auth_e::execute_edit_action( + action_str, + key, + login_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count, + ) + .await? + } else if app_state.ui.show_add_table { + add_table_e::execute_edit_action( + action_str, + key, + add_table_state, + ideal_cursor_column, + ) + .await? + } else if app_state.ui.show_register { + auth_e::execute_edit_action( action_str, key, register_state, ideal_cursor_column, grpc_client, current_position, - total_count - ).await? // Results in String + total_count, + ) + .await? } else { - // Return String directly, not Ok(String) - "Action not applicable here".to_string() - }; // Semicolon here ends the if/else expression + // Assuming FormState otherwise + form_e::execute_edit_action( + action_str, + key, + form_state, + ideal_cursor_column, + grpc_client, + current_position, + total_count + ).await? + }; + // Update role suggestions after deletion if needed (Register view) + if app_state.ui.show_register && register_state.current_field() == 4 { + register_state.update_role_suggestions(); + } - // Wrap the resulting String message return Ok(EditEventOutcome::Message(result_msg)); } diff --git a/client/src/modes/canvas/read_only.rs b/client/src/modes/canvas/read_only.rs index 292bf21..07ff8f9 100644 --- a/client/src/modes/canvas/read_only.rs +++ b/client/src/modes/canvas/read_only.rs @@ -6,8 +6,9 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::{canvas_state::CanvasState, auth::RegisterState}; use crate::state::pages::auth::LoginState; use crate::state::pages::form::FormState; +use crate::state::pages::add_table::AddTableState; use crate::state::app::state::AppState; -use crate::functions::modes::read_only::{auth_ro, form_ro}; +use crate::functions::modes::read_only::{auth_ro, form_ro, add_table_ro}; use crossterm::event::KeyEvent; pub async fn handle_read_only_event( @@ -17,6 +18,7 @@ pub async fn handle_read_only_event( form_state: &mut FormState, login_state: &mut LoginState, register_state: &mut RegisterState, + add_table_state: &mut AddTableState, key_sequence_tracker: &mut KeySequenceTracker, current_position: &mut u64, total_count: u64, @@ -32,34 +34,17 @@ pub async fn handle_read_only_event( } if config.is_enter_edit_mode_after(key.code, key.modifiers) { - let (current_input, current_pos) = if app_state.ui.show_login { // Check Login first - ( - login_state.get_current_input(), - login_state.current_cursor_pos(), - ) - } else if app_state.ui.show_register { // Then check Register - ( - register_state.get_current_input(), - register_state.current_cursor_pos(), - ) - } else { - ( - form_state.get_current_input(), - form_state.current_cursor_pos(), - ) // Default to Form - }; + // Determine target state to adjust cursor + let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } + else if app_state.ui.show_register { register_state } + else if app_state.ui.show_add_table { add_table_state } + else { form_state }; + let current_input = target_state.get_current_input(); + let current_pos = target_state.current_cursor_pos(); if !current_input.is_empty() && current_pos < current_input.len() { - if app_state.ui.show_login { - login_state.set_current_cursor_pos(current_pos + 1); - *ideal_cursor_column = login_state.current_cursor_pos(); - } else if app_state.ui.show_register { - register_state.set_current_cursor_pos(current_pos + 1); - *ideal_cursor_column = register_state.current_cursor_pos(); - } else { // Default to Form - form_state.set_current_cursor_pos(current_pos + 1); - *ideal_cursor_column = form_state.current_cursor_pos(); - } + target_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = target_state.current_cursor_pos(); } *edit_mode_cooldown = true; *command_message = "Entering Edit mode (after cursor)".to_string(); @@ -83,7 +68,7 @@ pub async fn handle_read_only_event( key_sequence_tracker.add_key(key.code); let sequence = key_sequence_tracker.get_sequence(); - if let Some(action) = config.matches_key_sequence_generalized(&sequence) { + if let Some(action) = config.matches_key_sequence_generalized(&sequence).as_deref() { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { crate::tui::functions::form::handle_action( action, @@ -96,6 +81,15 @@ pub async fn handle_read_only_event( .await? } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions crate::tui::functions::login::handle_action(action).await? + } else if app_state.ui.show_add_table { + add_table_ro::execute_action( + action, + app_state, + add_table_state, + ideal_cursor_column, + key_sequence_tracker, + command_message, + ).await? } else if app_state.ui.show_register{ auth_ro::execute_action( action, @@ -134,7 +128,7 @@ pub async fn handle_read_only_event( } 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) { + if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { crate::tui::functions::form::handle_action( action, @@ -147,6 +141,15 @@ pub async fn handle_read_only_event( .await? } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions crate::tui::functions::login::handle_action(action).await? + } else if app_state.ui.show_add_table { + add_table_ro::execute_action( + action, + app_state, + add_table_state, + ideal_cursor_column, + key_sequence_tracker, + command_message, + ).await? } else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions auth_ro::execute_action( action, @@ -184,7 +187,7 @@ pub async fn handle_read_only_event( } else { key_sequence_tracker.reset(); - if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) { + if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() { let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) { crate::tui::functions::form::handle_action( action, @@ -195,8 +198,17 @@ pub async fn handle_read_only_event( ideal_cursor_column, ) .await? - } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { // Handle login context actions + } else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) { crate::tui::functions::login::handle_action(action).await? + } else if app_state.ui.show_add_table { + add_table_ro::execute_action( + action, + app_state, + add_table_state, + ideal_cursor_column, + key_sequence_tracker, + command_message, + ).await? } else if app_state.ui.show_register /* && CONTEXT_ACTIONS_REGISTER.contains(&action) */ { // Handle register general actions auth_ro::execute_action( action, diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index a65eee5..ab43874 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -28,6 +28,7 @@ use crate::state::{ auth::{AuthState, LoginState, RegisterState}, admin::AdminState, canvas_state::CanvasState, + add_table::AddTableState, form::FormState, intro::IntroState, }, @@ -349,6 +350,7 @@ impl EventHandler { form_state, login_state, register_state, + &mut admin_state.add_table_state, &mut self.key_sequence_tracker, current_position, total_count, @@ -383,7 +385,7 @@ impl EventHandler { let (_should_exit, message) = read_only::handle_read_only_event( app_state, key, config, form_state, login_state, - register_state, &mut self.key_sequence_tracker, + register_state, &mut admin_state.add_table_state, &mut self.key_sequence_tracker, current_position, total_count, grpc_client, &mut self.command_message, &mut self.edit_mode_cooldown, &mut self.ideal_cursor_column, @@ -431,6 +433,7 @@ impl EventHandler { form_state, login_state, register_state, + &mut admin_state.add_table_state, &mut self.ideal_cursor_column, current_position, total_count, diff --git a/client/src/modes/handlers/mode_manager.rs b/client/src/modes/handlers/mode_manager.rs index 7afe049..3d5131a 100644 --- a/client/src/modes/handlers/mode_manager.rs +++ b/client/src/modes/handlers/mode_manager.rs @@ -2,6 +2,8 @@ use crate::state::app::state::AppState; use crate::modes::handlers::event::EventHandler; use crate::state::app::highlight::HighlightState; +use crate::state::pages::add_table::AddTableFocus; +use crate::state::pages::admin::AdminState; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppMode { @@ -29,17 +31,20 @@ impl ModeManager { return AppMode::General; } - if app_state.ui.show_login || app_state.ui.show_register { - if event_handler.is_edit_mode { - AppMode::Edit + let is_canvas_view = app_state.ui.show_login + || app_state.ui.show_register + || app_state.ui.show_form + || app_state.ui.show_add_table; + + if is_canvas_view { + if app_state.ui.focus_outside_canvas { + AppMode::General } else { - AppMode::ReadOnly - } - } else if app_state.ui.show_form { - if event_handler.is_edit_mode { - AppMode::Edit - } else { - AppMode::ReadOnly + if event_handler.is_edit_mode { + AppMode::Edit + } else { + AppMode::ReadOnly + } } } else { AppMode::General diff --git a/client/src/modes/highlight/highlight.rs b/client/src/modes/highlight/highlight.rs index c050be9..f4113b8 100644 --- a/client/src/modes/highlight/highlight.rs +++ b/client/src/modes/highlight/highlight.rs @@ -6,6 +6,7 @@ use crate::config::binds::key_sequences::KeySequenceTracker; use crate::services::grpc_client::GrpcClient; use crate::state::app::state::AppState; use crate::state::pages::auth::{LoginState, RegisterState}; +use crate::state::pages::add_table::AddTableState; use crate::state::pages::form::FormState; use crate::modes::handlers::event::EventOutcome; use crate::modes::read_only; // Import the ReadOnly handler @@ -21,6 +22,7 @@ pub async fn handle_highlight_event( form_state: &mut FormState, login_state: &mut LoginState, register_state: &mut RegisterState, + add_table_state: &mut AddTableState, key_sequence_tracker: &mut KeySequenceTracker, current_position: &mut u64, total_count: u64, @@ -38,6 +40,7 @@ pub async fn handle_highlight_event( form_state, login_state, register_state, + add_table_state, key_sequence_tracker, current_position, total_count, diff --git a/client/src/state/pages/add_table.rs b/client/src/state/pages/add_table.rs index fc661dd..a1b21f9 100644 --- a/client/src/state/pages/add_table.rs +++ b/client/src/state/pages/add_table.rs @@ -79,7 +79,7 @@ impl Default for AddTableState { } impl AddTableState { - const INPUT_FIELD_COUNT: usize = 3; + pub const INPUT_FIELD_COUNT: usize = 3; } // Implement CanvasState for the input fields