// src/ui/handlers/ui.rs use crate::config::binds::config::Config; use crate::config::colors::themes::Theme; use crate::services::grpc_client::GrpcClient; use crate::services::ui_service::UiService; use crate::config::storage::storage::load_auth_data; use crate::modes::common::commands::CommandHandler; use crate::modes::handlers::event::{EventHandler, EventOutcome}; use crate::modes::handlers::mode_manager::{AppMode, ModeManager}; use crate::state::pages::auth::AuthState; use crate::state::pages::auth::UserRole; use crate::pages::login::LoginFormState; use crate::pages::register::RegisterFormState; use crate::pages::admin_panel::add_table; use crate::pages::admin_panel::add_logic; use crate::pages::admin::AdminState; use crate::pages::admin::AdminFocus; use crate::pages::admin::admin; use crate::pages::intro::IntroState; use crate::pages::forms::{FormState, FieldDefinition}; use crate::pages::forms; use crate::pages::routing::{Router, Page}; use crate::buffer::state::BufferState; use crate::buffer::state::AppView; use crate::state::app::state::AppState; use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; use crate::input::leader::leader_has_any_start; use crate::pages::login; use crate::pages::register; use crate::pages::login::LoginResult; use crate::pages::login::LoginState; use crate::pages::register::RegisterResult; use crate::ui::handlers::context::DialogPurpose; use crate::utils::columns::filter_user_columns; use canvas::keymap::KeyEventOutcome; use canvas::CursorManager; use canvas::FormEditor; use anyhow::{Context, Result}; use crossterm::cursor::{SetCursorStyle, MoveTo}; use crossterm::event as crossterm_event; use crossterm::ExecutableCommand; use tracing::{error, info, warn}; use tokio::sync::mpsc; use std::time::Instant; use std::time::Duration; #[cfg(feature = "ui-debug")] use crate::state::app::state::DebugState; #[cfg(feature = "ui-debug")] use crate::utils::debug_logger::pop_next_debug_message; // Rest of the file remains the same... pub async fn run_ui() -> Result<()> { let config = Config::load().context("Failed to load configuration")?; let theme = Theme::from_str(&config.colors.theme); let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?; let mut grpc_client = GrpcClient::new().await.context("Failed to create GrpcClient")?; let mut command_handler = CommandHandler::new(); let (login_result_sender, mut login_result_receiver) = mpsc::channel::(1); let (register_result_sender, mut register_result_receiver) = mpsc::channel::(1); let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::>(1); let (save_logic_result_sender, _save_logic_result_receiver) = mpsc::channel::>(1); let mut event_handler = EventHandler::new( login_result_sender.clone(), register_result_sender.clone(), save_table_result_sender.clone(), save_logic_result_sender.clone(), grpc_client.clone(), ) .await .context("Failed to create event handler")?; let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); let mut login_state = LoginFormState::new(); login_state.editor.set_keymap(config.build_canvas_keymap()); let mut register_state = RegisterFormState::default(); register_state.editor.set_keymap(config.build_canvas_keymap()); let mut intro_state = IntroState::default(); let mut admin_state = AdminState::default(); let mut router = Router::new(); let mut buffer_state = BufferState::default(); let mut app_state = AppState::new().context("Failed to create initial app state")?; let mut auto_logged_in = false; match load_auth_data() { Ok(Some(stored_data)) => { auth_state.auth_token = Some(stored_data.access_token); auth_state.user_id = Some(stored_data.user_id); auth_state.role = Some(UserRole::from_str(&stored_data.role)); auth_state.decoded_username = Some(stored_data.username); auto_logged_in = true; info!("Auth data loaded from file. User is auto-logged in."); } Ok(None) => { info!("No stored auth data found. User will see intro/login."); } Err(e) => { error!("Failed to load auth data: {}", e); } } let (initial_profile, initial_table, initial_columns_from_service) = UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) .await .context("Failed to initialize app state and form")?; let initial_field_defs: Vec = filter_user_columns(initial_columns_from_service) .into_iter() .map(|col_name| FieldDefinition { display_name: col_name.clone(), data_key: col_name, is_link: false, link_target_table: None, }) .collect(); // Replace local form_state with app_state.form_editor let path = format!("{}/{}", initial_profile, initial_table); app_state.ensure_form_editor(&path, &config, || { FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs) }); #[cfg(feature = "validation")] UiService::apply_validation1_for_form(&mut grpc_client, &mut app_state, &path) .await .ok(); buffer_state.update_history(AppView::Form(path.clone())); router.navigate(Page::Form(path.clone())); // Fetch initial count using app_state accessor if let Some(form_state) = app_state.form_state_for_path(&path) { 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, form_state).await { event_handler.command_message = format!("Error loading initial data: {}", e); } } else { form_state.reset_to_empty(); } } if auto_logged_in { let path = format!("{}/{}", initial_profile, initial_table); buffer_state.history = vec![AppView::Form(path.clone())]; router.navigate(Page::Form(path)); buffer_state.active_index = 0; info!("Initial view set to Form due to auto-login."); } let mut last_frame_time = Instant::now(); let mut current_fps = 0.0; let mut needs_redraw = true; let mut prev_view_profile_name = app_state.current_view_profile_name.clone(); let mut prev_view_table_name = app_state.current_view_table_name.clone(); let mut table_just_switched = false; loop { let position_before_event = if let Page::Form(path) = &router.current { app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1) } else { 1 }; let mut event_processed = false; // --- CHANNEL RECEIVERS --- // For main search palette match event_handler.search_result_receiver.try_recv() { Ok(hits) => { info!("--- 4. Main loop received message from channel. ---"); if let Some(search_state) = app_state.search_state.as_mut() { search_state.results = hits; search_state.is_loading = false; } needs_redraw = true; } Err(mpsc::error::TryRecvError::Empty) => { } Err(mpsc::error::TryRecvError::Disconnected) => { error!("Search result channel disconnected!"); } } // --- ADDED: For live form autocomplete --- match event_handler.autocomplete_result_receiver.try_recv() { Ok(hits) => { if let Page::Form(path) = &router.current { if let Some(form_state) = app_state.form_state_for_path(path) { 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()); } } } needs_redraw = true; } Err(mpsc::error::TryRecvError::Empty) => {} Err(mpsc::error::TryRecvError::Disconnected) => { error!("Autocomplete result channel disconnected!"); } } 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; // Decouple Command Line and palettes from canvas: // Only forward keys to Form canvas when: // - not in command mode // - no search/palette active // - focus is inside the canvas if let crossterm_event::Event::Key(key_event) = &event { let overlay_active = event_handler.command_mode || app_state.ui.show_search_palette || event_handler.navigation_state.active; if !overlay_active { let inside_canvas = match &router.current { Page::Form(_) => true, Page::Login(state) => !state.focus_outside_canvas, Page::Register(state) => !state.focus_outside_canvas, Page::AddTable(state) => !state.focus_outside_canvas, Page::AddLogic(state) => !state.focus_outside_canvas, _ => false, }; if inside_canvas { // Do NOT forward to canvas while a leader is active or about to start. // This prevents the canvas from stealing the second/third key (b/d/r). let leader_in_progress = event_handler.input_engine.has_active_sequence(); let is_space = matches!(key_event.code, crossterm_event::KeyCode::Char(' ')); let can_start_leader = leader_has_any_start(&config); let form_in_edit_mode = match &router.current { Page::Form(path) => app_state .editor_for_path_ref(path) .map(|e| e.mode() == canvas::AppMode::Edit) .unwrap_or(false), _ => false, }; let defer_to_engine_for_leader = leader_in_progress || (is_space && can_start_leader && !form_in_edit_mode); if defer_to_engine_for_leader { info!( "Skipping canvas pre-handle: leader sequence active or starting" ); } else { if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { match editor.handle_key_event(*key_event) { KeyEventOutcome::Consumed(Some(msg)) => { event_handler.command_message = msg; needs_redraw = true; continue; } KeyEventOutcome::Consumed(None) => { needs_redraw = true; continue; } KeyEventOutcome::Pending => { needs_redraw = true; continue; } KeyEventOutcome::NotMatched => { // fall through to client-level handling } } } } } } } } // Call handle_event directly let event_outcome_result = event_handler.handle_event( event, &config, &mut terminal, &mut command_handler, &mut auth_state, &mut buffer_state, &mut app_state, &mut router, ).await; let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome { EventOutcome::Ok(message) => { if !message.is_empty() { event_handler.command_message = message; } } EventOutcome::Exit(message) => { event_handler.command_message = message; should_exit = true; } EventOutcome::DataSaved(save_outcome, message) => { event_handler.command_message = message; if let Page::Form(path) = &router.current { if let Some(mut temp_form_state) = app_state.form_state_for_path(path).cloned() { if let Err(e) = UiService::handle_save_outcome( save_outcome, &mut grpc_client, &mut app_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_for_path(path) { *form_state = temp_form_state; } } } } EventOutcome::ButtonSelected { .. } => {} EventOutcome::TableSelected { path } => { let parts: Vec<&str> = path.split('/').collect(); if parts.len() == 2 { let profile_name = parts[0].to_string(); let table_name = parts[1].to_string(); app_state.set_current_view_table(profile_name, table_name); buffer_state.update_history(AppView::Form(path.clone())); event_handler.command_message = format!("Loading table: {}", path); } else { event_handler.command_message = format!("Invalid table path: {}", path); } } }, Err(e) => { event_handler.command_message = format!("Error: {}", e); } } if should_exit { return Ok(()); } } match login_result_receiver.try_recv() { Ok(result) => { // Apply result to the active router Login page if present, // otherwise update the local copy. let updated = if let Page::Login(page) = &mut router.current { login::handle_login_result( result, &mut app_state, &mut auth_state, page, ) } else { login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) }; if updated { needs_redraw = true; } } Err(mpsc::error::TryRecvError::Empty) => {} Err(mpsc::error::TryRecvError::Disconnected) => { error!("Login result channel disconnected unexpectedly."); } } match register_result_receiver.try_recv() { Ok(result) => { if register::handle_registration_result(result, &mut app_state, &mut register_state) { needs_redraw = true; } } Err(mpsc::error::TryRecvError::Empty) => {} Err(mpsc::error::TryRecvError::Disconnected) => { error!("Register result channel disconnected unexpectedly."); } } match save_table_result_receiver.try_recv() { Ok(result) => { app_state.hide_dialog(); match result { Ok(ref success_message) => { app_state.show_dialog( "Save Successful", success_message, vec!["OK".to_string()], DialogPurpose::SaveTableSuccess, ); if let Page::AddTable(page) = &mut router.current { page.state.has_unsaved_changes = false; } } Err(e) => { event_handler.command_message = format!("Save failed: {}", e); } } needs_redraw = true; } Err(mpsc::error::TryRecvError::Empty) => {} Err(mpsc::error::TryRecvError::Disconnected) => { error!("Save table result channel disconnected unexpectedly."); } } if let Some(active_view) = buffer_state.get_active_view() { match active_view { AppView::Intro => { // Keep external intro_state in sync with the live Router state if let Page::Intro(current) = &router.current { intro_state = current.clone(); } // Navigate with the up-to-date state router.navigate(Page::Intro(intro_state.clone())); } AppView::Login => { // Do not re-create the page every frame. If we're already on Login, // keep it. If we just switched into Login, create it once and // inject the keymap. if let Page::Login(_) = &router.current { // Already on login page; keep existing state } else { let mut page = LoginFormState::new(); page.editor.set_keymap(config.build_canvas_keymap()); router.navigate(Page::Login(page)); } } AppView::Register => { if let Page::Register(_) = &router.current { // already on register page } else { let mut page = RegisterFormState::new(); page.editor.set_keymap(config.build_canvas_keymap()); router.navigate(Page::Register(page)); } } AppView::Admin => { if let Page::Admin(current) = &router.current { admin_state = current.clone(); } info!("Auth role at render: {:?}", auth_state.role); // Use the admin loader instead of inline logic if let Err(e) = admin::loader::refresh_admin_state(&mut grpc_client, &mut app_state, &mut admin_state).await { error!("Failed to refresh admin state: {}", e); event_handler.command_message = format!("Error refreshing admin data: {}", e); } router.navigate(Page::Admin(admin_state.clone())); } AppView::AddTable => { if let Page::AddTable(page) = &mut router.current { // Ensure keymap is set once (same as AddLogic) page.editor.set_keymap(config.build_canvas_keymap()); } else { // Page is created by admin navigation (Button2). No-op here. } } AppView::AddLogic => { if let Page::AddLogic(page) = &mut router.current { // Ensure keymap is set once page.editor.set_keymap(config.build_canvas_keymap()); } } AppView::Form(path) => { // Keep current_view_* consistent with the active buffer path if let Some((profile, table)) = path.split_once('/') { app_state.set_current_view_table( profile.to_string(), table.to_string(), ); } router.navigate(Page::Form(path.clone())); } AppView::Scratch => {} } } if let Page::Form(_current_path) = &router.current { let current_view_profile = app_state.current_view_profile_name.clone(); let current_view_table = app_state.current_view_table_name.clone(); if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table { if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) { app_state.show_loading_dialog( "Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name), ); needs_redraw = true; // DELEGATE to the forms loader match forms::loader::ensure_form_loaded_and_count( &mut grpc_client, &mut app_state, &config, prof_name, tbl_name, ).await { Ok(()) => { app_state.hide_dialog(); prev_view_profile_name = current_view_profile; prev_view_table_name = current_view_table; table_just_switched = true; // Apply character-limit validation for the new form #[cfg(feature = "validation")] if let (Some(prof), Some(tbl)) = ( app_state.current_view_profile_name.as_ref(), app_state.current_view_table_name.as_ref(), ) { let p = format!("{}/{}", prof, tbl); UiService::apply_validation1_for_form( &mut grpc_client, &mut app_state, &p, ) .await .ok(); } } Err(e) => { app_state.update_dialog_content( &format!("Error loading table: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed, ); // Reset to previous state on error app_state.current_view_profile_name = prev_view_profile_name.clone(); app_state.current_view_table_name = prev_view_table_name.clone(); } } } needs_redraw = true; } } let needs_redraw_from_fetch = add_logic::loader::process_pending_table_structure_fetch( &mut app_state, &mut router, &mut grpc_client, &mut event_handler.command_message, ).await.unwrap_or(false); if needs_redraw_from_fetch { needs_redraw = true; } if let Page::AddLogic(state) = &mut router.current { let needs_redraw_from_columns = add_logic::loader::maybe_fetch_columns_for_awaiting_table( &mut grpc_client, state, &mut event_handler.command_message, ).await.unwrap_or(false); if needs_redraw_from_columns { needs_redraw = true; } } let current_position = if let Page::Form(path) = &router.current { app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1) } else { 1 }; let position_changed = current_position != position_before_event; let mut position_logic_needs_redraw = false; if let Page::Form(path) = &router.current { if !table_just_switched { if position_changed && !app_state.is_canvas_edit_mode_at(path) { position_logic_needs_redraw = true; if let Some(form_state) = app_state.form_state_for_path(path) { 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); } } } 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 && !app_state.is_canvas_edit_mode_at(path) { if let Some(form_state) = app_state.form_state_for_path(path) { 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 let Page::Register(state) = &mut router.current { if !app_state.is_canvas_edit_mode() { let current_input = state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos)); } } else if let Page::Login(state) = &mut router.current { if !app_state.is_canvas_edit_mode() { let current_input = state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos)); } } if position_logic_needs_redraw { needs_redraw = true; } if app_state.ui.dialog.is_loading { needs_redraw = true; } #[cfg(feature = "ui-debug")] { let can_display_next = match &app_state.debug_state { Some(current) => current.display_start_time.elapsed() >= Duration::from_secs(2), None => true, }; if can_display_next { if let Some((new_message, is_error)) = pop_next_debug_message() { app_state.debug_state = Some(DebugState { displayed_message: new_message, is_error, display_start_time: Instant::now(), }); } } } if event_processed || needs_redraw || position_changed { let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router); match current_mode { AppMode::General => { let outside_canvas = match &router.current { Page::Login(state) => state.focus_outside_canvas, Page::Register(state) => state.focus_outside_canvas, Page::AddTable(state) => state.focus_outside_canvas, Page::AddLogic(state) => state.focus_outside_canvas, _ => false, // Form and Admin don’t use this flag }; if outside_canvas { // Outside canvas → app decides terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; } else { // Inside canvas → let canvas handle it if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { let _ = CursorManager::update_for_mode(editor.mode()); } } if let Page::Login(page) = &router.current { let _ = CursorManager::update_for_mode(page.editor.mode()); } if let Page::Register(page) = &router.current { let _ = CursorManager::update_for_mode(page.editor.mode()); } if let Page::AddTable(page) = &router.current { let _ = CursorManager::update_for_mode(page.editor.mode()); } if let Page::AddLogic(page) = &router.current { let _ = CursorManager::update_for_mode(page.editor.mode()); } } } AppMode::Command => { // Command line overlay → app decides terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; } } let current_dir = app_state.current_dir.clone(); terminal .draw(|f| { render_ui( f, &mut router, &buffer_state, &theme, &event_handler.command_input, event_handler.command_mode, &event_handler.command_message, &event_handler.navigation_state, ¤t_dir, current_fps, &app_state, &auth_state, ); }) .context("Terminal draw call failed")?; needs_redraw = false; } let now = Instant::now(); let frame_duration = now.duration_since(last_frame_time); last_frame_time = now; if frame_duration.as_secs_f64() > 1e-6 { current_fps = 1.0 / frame_duration.as_secs_f64(); } table_just_switched = false; } }