diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index 0df1a76..1987d77 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -7,8 +7,9 @@ use common::proto::komp_ac::table_structure::TableStructureResponse; use crate::modes::handlers::mode_manager::AppMode; use crate::state::app::search::SearchState; use crate::ui::handlers::context::DialogPurpose; -use canvas::FormEditor; use crate::state::pages::form::FormState; +use crate::config::binds::Config; +use canvas::FormEditor; use std::collections::HashMap; use std::env; use std::sync::Arc; @@ -185,6 +186,29 @@ impl AppState { .get(self.ui.dialog.dialog_active_button_index) .map(|s| s.as_str()) } + + pub fn init_form_editor(&mut self, form_state: FormState, config: &Config) { + let mut editor = FormEditor::new(form_state); + editor.set_keymap(config.build_canvas_keymap()); // inject keymap + self.form_editor = Some(editor); + } + + /// Replace the current form state and wrap it in a FormEditor with keymap + pub fn set_form_state(&mut self, form_state: FormState, config: &Config) { + let mut editor = FormEditor::new(form_state); + editor.set_keymap(config.build_canvas_keymap()); + self.form_editor = Some(editor); + } + + /// Immutable access to the underlying FormState + pub fn form_state(&self) -> Option<&FormState> { + self.form_editor.as_ref().map(|e| e.data_provider()) + } + + /// Mutable access to the underlying FormState + pub fn form_state_mut(&mut self) -> Option<&mut FormState> { + self.form_editor.as_mut().map(|e| e.data_provider_mut()) + } } impl Default for UiState { diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index cded381..151b472 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -116,26 +116,6 @@ impl FormState { } } - pub fn render( - &self, - f: &mut Frame, - area: Rect, - theme: &Theme, - is_edit_mode: bool, - ) { - // Wrap in FormEditor for new API - let mut editor = FormEditor::new(self.clone()); - - // Use new canvas rendering - canvas::render_canvas_default(f, area, &editor); - - // If autocomplete is active, render suggestions - if self.autocomplete_active && !self.autocomplete_suggestions.is_empty() { - // Note: This will need to be updated when suggestions are integrated - // canvas::render_suggestions_dropdown(f, area, input_rect, &canvas::DefaultCanvasTheme, &editor); - } - } - pub fn reset_to_empty(&mut self) { self.id = 0; self.values.iter_mut().for_each(|v| v.clear()); diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index a528cf8..df8c6a7 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -24,6 +24,7 @@ use crate::state::pages::auth::LoginState; use crate::state::pages::auth::RegisterState; use crate::state::pages::form::FormState; use crate::state::pages::intro::IntroState; +use crate::components::render_form; use ratatui::{ layout::{Constraint, Direction, Layout}, Frame, @@ -192,12 +193,15 @@ pub fn render_ui( .split(form_actual_area)[1] }; - // CHANGED: Convert local HighlightState to canvas HighlightState for FormState - form_state.render( + render_form( f, form_render_area, + app_state, + form_state, + app_state.current_view_table_name.as_deref().unwrap_or(""), theme, - is_event_handler_edit_mode, + form_state.total_count, + form_state.current_position, ); } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 96d3a7d..d22a4cb 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -102,25 +102,28 @@ pub async fn run_ui() -> Result<()> { }) .collect(); - let mut form_state = FormState::new( - initial_profile.clone(), - initial_table.clone(), - initial_field_defs, + // Replace local form_state with app_state.form_editor + app_state.set_form_state( + FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs), + &config, ); - UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state) - .await - .context(format!( - "Failed to fetch initial count for table {}.{}", - initial_profile, initial_table - ))?; + // Fetch initial count using app_state accessor + if let Some(form_state) = app_state.form_state_mut() { + UiService::fetch_and_set_table_count(&mut grpc_client, form_state) + .await + .context(format!( + "Failed to fetch initial count for table {}.{}", + initial_profile, initial_table + ))?; - if form_state.total_count > 0 { - if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { - event_handler.command_message = format!("Error loading initial data: {}", e); + if form_state.total_count > 0 { + if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, form_state).await { + event_handler.command_message = format!("Error loading initial data: {}", e); + } + } else { + form_state.reset_to_empty(); } - } else { - form_state.reset_to_empty(); } if auto_logged_in { @@ -137,7 +140,9 @@ pub async fn run_ui() -> Result<()> { let mut table_just_switched = false; loop { - let position_before_event = form_state.current_position; + let position_before_event = app_state.form_state() + .map(|fs| fs.current_position) + .unwrap_or(1); let mut event_processed = false; // --- CHANNEL RECEIVERS --- @@ -162,15 +167,17 @@ pub async fn run_ui() -> Result<()> { // --- ADDED: For live form autocomplete --- match event_handler.autocomplete_result_receiver.try_recv() { Ok(hits) => { - if form_state.autocomplete_active { - form_state.autocomplete_suggestions = hits; - form_state.autocomplete_loading = false; - if !form_state.autocomplete_suggestions.is_empty() { - form_state.selected_suggestion_index = Some(0); - } else { - form_state.selected_suggestion_index = None; + if let Some(form_state) = app_state.form_state_mut() { + if form_state.autocomplete_active { + form_state.autocomplete_suggestions = hits; + form_state.autocomplete_loading = false; + if !form_state.autocomplete_suggestions.is_empty() { + form_state.selected_suggestion_index = Some(0); + } else { + form_state.selected_suggestion_index = None; + } + event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len()); } - event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len()); } needs_redraw = true; } @@ -180,27 +187,38 @@ pub async fn run_ui() -> Result<()> { } } - if app_state.ui.show_search_palette { needs_redraw = true; } if crossterm_event::poll(std::time::Duration::from_millis(1))? { let event = event_reader.read_event().context("Failed to read terminal event")?; event_processed = true; - let event_outcome_result = event_handler.handle_event( - event, - &config, - &mut terminal, - &mut command_handler, - &mut form_state, - &mut auth_state, - &mut login_state, - &mut register_state, - &mut intro_state, - &mut admin_state, - &mut buffer_state, - &mut app_state, - ).await; + let event_outcome_result = { + // We need to avoid borrowing app_state twice, so we'll need to modify the handle_event call + // For now, let's create a temporary approach + let mut temp_form_state = app_state.form_state_mut().unwrap().clone(); + let result = event_handler.handle_event( + event, + &config, + &mut terminal, + &mut command_handler, + &mut temp_form_state, + &mut auth_state, + &mut login_state, + &mut register_state, + &mut intro_state, + &mut admin_state, + &mut buffer_state, + &mut app_state, + ).await; + + // Update the app_state with any changes from temp_form_state + if let Some(form_state) = app_state.form_state_mut() { + *form_state = temp_form_state; + } + + result + }; let mut should_exit = false; match event_outcome_result { @@ -216,15 +234,21 @@ pub async fn run_ui() -> Result<()> { } EventOutcome::DataSaved(save_outcome, message) => { event_handler.command_message = message; + // Clone form_state to avoid double borrow + let mut temp_form_state = app_state.form_state().unwrap().clone(); if let Err(e) = UiService::handle_save_outcome( save_outcome, &mut grpc_client, &mut app_state, - &mut form_state, + &mut temp_form_state, ).await { event_handler.command_message = format!("Error handling save outcome: {}", e); } + // Update app_state with changes + if let Some(form_state) = app_state.form_state_mut() { + *form_state = temp_form_state; + } } EventOutcome::ButtonSelected { .. } => {} EventOutcome::TableSelected { path } => { @@ -348,7 +372,7 @@ pub async fn run_ui() -> Result<()> { // Continue with the rest of the function... // (The rest remains the same, but now CanvasState trait methods are available) - + if app_state.ui.show_form { let current_view_profile = app_state.current_view_profile_name.clone(); let current_view_table = app_state.current_view_table_name.clone(); @@ -373,39 +397,43 @@ pub async fn run_ui() -> Result<()> { ) .await { - Ok(mut new_form_state) => { - if let Err(e) = UiService::fetch_and_set_table_count( - &mut grpc_client, - &mut new_form_state, - ) - .await - { - app_state.update_dialog_content( - &format!("Error fetching count: {}", e), - vec!["OK".to_string()], - DialogPurpose::LoginFailed, - ); - } else if new_form_state.total_count > 0 { - if let Err(e) = UiService::load_table_data_by_position( + Ok(new_form_state) => { + // Set the new form state and fetch count + app_state.set_form_state(new_form_state, &config); + + if let Some(form_state) = app_state.form_state_mut() { + if let Err(e) = UiService::fetch_and_set_table_count( &mut grpc_client, - &mut new_form_state, + form_state, ) .await { app_state.update_dialog_content( - &format!("Error loading data: {}", e), + &format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed, ); + } else if form_state.total_count > 0 { + if let Err(e) = UiService::load_table_data_by_position( + &mut grpc_client, + form_state, + ) + .await + { + app_state.update_dialog_content( + &format!("Error loading data: {}", e), + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + } else { + app_state.hide_dialog(); + } } else { + form_state.reset_to_empty(); app_state.hide_dialog(); } - } else { - new_form_state.reset_to_empty(); - app_state.hide_dialog(); } - form_state = new_form_state; prev_view_profile_name = current_view_profile; prev_view_table_name = current_view_table; table_just_switched = true; @@ -429,7 +457,7 @@ pub async fn run_ui() -> Result<()> { // Continue with the rest of the positioning logic... // Now we can use CanvasState methods like get_current_input(), current_field(), etc. - + if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { if app_state.ui.show_add_logic { if admin_state.add_logic_state.profile_name == profile_name && @@ -488,47 +516,53 @@ pub async fn run_ui() -> Result<()> { } } - let position_changed = form_state.current_position != position_before_event; + let current_position = app_state.form_state() + .map(|fs| fs.current_position) + .unwrap_or(1); + let position_changed = current_position != position_before_event; let mut position_logic_needs_redraw = false; if app_state.ui.show_form && !table_just_switched { if position_changed && !event_handler.is_edit_mode { position_logic_needs_redraw = true; - if form_state.current_position > form_state.total_count { - form_state.reset_to_empty(); - event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name); - } else { - match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { - Ok(load_message) => { - if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { - event_handler.command_message = load_message; + if let Some(form_state) = app_state.form_state_mut() { + if form_state.current_position > form_state.total_count { + form_state.reset_to_empty(); + event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name); + } else { + match UiService::load_table_data_by_position(&mut grpc_client, form_state).await { + Ok(load_message) => { + if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { + event_handler.command_message = load_message; + } + } + Err(e) => { + event_handler.command_message = format!("Error loading data: {}", e); } } - Err(e) => { - event_handler.command_message = format!("Error loading data: {}", e); - } } + + let current_input_after_load_str = form_state.get_current_input(); + let current_input_len_after_load = current_input_after_load_str.chars().count(); + let max_cursor_pos = if current_input_len_after_load > 0 { + current_input_len_after_load.saturating_sub(1) + } else { + 0 + }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } - - let current_input_after_load_str = form_state.get_current_input(); - let current_input_len_after_load = current_input_after_load_str.chars().count(); - let max_cursor_pos = if current_input_len_after_load > 0 { - current_input_len_after_load.saturating_sub(1) - } else { - 0 - }; - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); - } else if !position_changed && !event_handler.is_edit_mode { - let current_input_str = form_state.get_current_input(); - let current_input_len = current_input_str.chars().count(); - let max_cursor_pos = if current_input_len > 0 { - current_input_len.saturating_sub(1) - } else { - 0 - }; - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); + if let Some(form_state) = app_state.form_state_mut() { + let current_input_str = form_state.get_current_input(); + let current_input_len = current_input_str.chars().count(); + let max_cursor_pos = if current_input_len > 0 { + current_input_len.saturating_sub(1) + } else { + 0 + }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); + } } } else if app_state.ui.show_register { if !event_handler.is_edit_mode { @@ -587,10 +621,23 @@ pub async fn run_ui() -> Result<()> { AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; } } + // Temporarily work around borrow checker by extracting needed values + let current_dir = app_state.current_dir.clone(); + + // Since we can't borrow app_state both mutably and immutably, + // we'll need to either: + // 1. Modify render_ui to take just app_state and access form_state internally, OR + // 2. Extract the specific fields render_ui needs from app_state + + // For now, using approach where we temporarily clone what we need + let form_state_clone = app_state.form_state().unwrap().clone(); + terminal.draw(|f| { + // Use a mutable clone for rendering + let mut temp_form_state = form_state_clone.clone(); render_ui( f, - &mut form_state, + &mut temp_form_state, &mut auth_state, &login_state, ®ister_state, @@ -603,10 +650,13 @@ pub async fn run_ui() -> Result<()> { event_handler.command_mode, &event_handler.command_message, &event_handler.navigation_state, - &app_state.current_dir, + ¤t_dir, current_fps, &app_state, ); + + // If render_ui modified the form_state, we'd need to sync it back + // But typically render functions don't modify state, just read it }).context("Terminal draw call failed")?; needs_redraw = false; }