// 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::auth::AuthClient; // <-- Add AuthClient import use crate::services::ui_service::UiService; 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::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::state::pages::auth::AuthState; use crate::state::pages::auth::LoginState; use crate::state::pages::auth::RegisterState; use crate::state::pages::admin::AdminState; use crate::state::pages::intro::IntroState; use crate::state::app::buffer::BufferState; use crate::state::app::buffer::AppView; use crate::state::app::state::AppState; use crate::ui::handlers::context::DialogPurpose; // <-- Add DialogPurpose import // Import SaveOutcome use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; use crate::tui::functions::common::login; // <-- Add login module import use std::time::Instant; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; use tracing::{info, error}; pub async fn run_ui() -> Result<(), Box> { let config = Config::load()?; let theme = Theme::from_str(&config.colors.theme); let mut terminal = TerminalCore::new()?; let mut grpc_client = GrpcClient::new().await?; let mut command_handler = CommandHandler::new(); let mut event_handler = EventHandler::new().await?; let event_reader = EventReader::new(); let mut auth_state = AuthState::default(); let mut login_state = LoginState::default(); let mut register_state = RegisterState::default(); let mut intro_state = IntroState::default(); let mut admin_state = AdminState::default(); let mut buffer_state = BufferState::default(); let mut app_state = AppState::new()?; // Initialize app state with profile tree and table structure let column_names = UiService::initialize_app_state(&mut grpc_client, &mut app_state) .await?; let mut form_state = FormState::new(column_names); // Fetch the total count of Adresar entries UiService::initialize_adresar_count(&mut grpc_client, &mut app_state) .await?; form_state.reset_to_empty(); // --- FPS Calculation State --- let mut last_frame_time = Instant::now(); let mut current_fps = 0.0; loop { // --- Synchronize UI View from Active Buffer --- if let Some(active_view) = buffer_state.get_active_view() { // Reset all flags first app_state.ui.show_intro = false; app_state.ui.show_login = false; app_state.ui.show_register = false; app_state.ui.show_admin = false; app_state.ui.show_add_table = false; app_state.ui.show_form = false; match active_view { AppView::Intro => app_state.ui.show_intro = true, AppView::Login => app_state.ui.show_login = true, AppView::Register => app_state.ui.show_register = true, AppView::Admin => { app_state.ui.show_admin = true; let profile_names = app_state.profile_tree.profiles.iter() .map(|p| p.name.clone()) .collect(); admin_state.set_profiles(profile_names); } AppView::AddTable => app_state.ui.show_add_table = true, AppView::Form(_) => app_state.ui.show_form = true, AppView::Scratch => {} // Or show a scratchpad component } } // --- End Synchronization --- // --- 3. Draw UI --- // Draw the current state *first*. This ensures the loading dialog // set in the *previous* iteration gets rendered before the pending // action check below. terminal.draw(|f| { render_ui( f, &mut form_state, &mut auth_state, &login_state, ®ister_state, &intro_state, &mut admin_state, &buffer_state, &theme, event_handler.is_edit_mode, // Use event_handler's state &event_handler.highlight_state, app_state.total_count, app_state.current_position, &app_state.current_dir, &event_handler.command_input, event_handler.command_mode, &event_handler.command_message, current_fps, &app_state, ); })?; // --- Cursor Visibility Logic --- // (Keep existing cursor logic here - depends on state drawn above) let current_mode = ModeManager::derive_mode(&app_state, &event_handler); match current_mode { AppMode::Edit => { terminal.show_cursor()?; } AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; } AppMode::ReadOnly => { if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; } else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; } terminal.show_cursor()?; } AppMode::General => { if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; } else { terminal.hide_cursor()?; } } AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; } } // --- End Cursor Visibility Logic --- // --- 2. Check for Pending Login Action --- if login_state.login_request_pending { // Reset the flag *before* calling save login_state.login_request_pending = false; // Create AuthClient and call save match AuthClient::new().await { Ok(mut auth_client_instance) => { // Call the ORIGINAL save function from the login module let save_result = login::save( &mut auth_state, &mut login_state, &mut auth_client_instance, // Pass the new client instance &mut app_state, ).await; // Use tracing for logging the outcome match save_result { // save returns Result, Ok contains the message Ok(msg) => info!(message = %msg, "Login save result"), // Use tracing::info! Err(e) => error!(error = %e, "Error during login save"), // Use tracing::error! } // Note: save already handles showing the final dialog (success/failure) } Err(e) => { // Handle client connection error - show dialog directly // Ensure flag is already false here app_state.show_dialog( // Use show_dialog, not update_dialog_content "Login Failed", &format!("Connection Error: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed, // Use appropriate purpose ); login_state.error_message = Some(format!("Connection Error: {}", e)); error!(error = %e, "Failed to create AuthClient"); // Use tracing::error! } } // After save runs, the state (dialog content, etc.) is updated. // The *next* iteration's draw call will show the final result. } // --- End Pending Login Check --- let total_count = app_state.total_count; let mut current_position = app_state.current_position; let position_before_event = current_position; // --- 1. Handle Terminal Events --- let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); // Poll for events *after* drawing and checking pending actions if crossterm_event::poll(std::time::Duration::from_millis(20))? { let event = event_reader.read_event()?; event_outcome_result = event_handler .handle_event( event, &config, &mut terminal, &mut grpc_client, &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, total_count, &mut current_position, ) .await; } // Update position based on handler's modification // This happens *after* the event is handled app_state.current_position = current_position; // --- Centralized Consequence Handling --- let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome { EventOutcome::Ok(message) => { if !message.is_empty() { // Update command message only if event handling produced one // Avoid overwriting messages potentially set by pending actions // 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; // Show save status if let Err(e) = UiService::handle_save_outcome( save_outcome, &mut grpc_client, &mut app_state, &mut form_state, ) .await { event_handler.command_message = format!("Error handling save outcome: {}", e); } } EventOutcome::ButtonSelected { context: _, index: _ } => { // This case should ideally be fully handled within handle_event // If initiate_login was called, it returned early. // If not, the message was set and returned via Ok(message). // Log if necessary, but likely no action needed here. // log::warn!("ButtonSelected outcome reached main loop unexpectedly."); } }, Err(e) => { event_handler.command_message = format!("Error: {}", e); } } // --- End Consequence Handling --- // --- Position Change Handling (after outcome processing and pending actions) --- let position_changed = app_state.current_position != position_before_event; let current_total_count = app_state.total_count; if app_state.ui.show_form { if position_changed && !event_handler.is_edit_mode { let current_input = form_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 // Limit to last character in readonly mode } else { 0 }; form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); // Ensure position never exceeds total_count + 1 if app_state.current_position > current_total_count + 1 { app_state.current_position = current_total_count + 1; } if app_state.current_position > current_total_count { // New entry - reset form form_state.reset_to_empty(); form_state.current_field = 0; } else if app_state.current_position >= 1 && app_state.current_position <= current_total_count { // Existing entry - load data let current_position_to_load = app_state.current_position; // Use a copy let load_message = UiService::load_adresar_by_position( &mut grpc_client, &mut app_state, // Pass app_state mutably if needed by the service &mut form_state, current_position_to_load, ) .await?; let current_input = form_state.get_current_input(); let max_cursor_pos = if !event_handler.is_edit_mode && !current_input.is_empty() { current_input.len() - 1 // In readonly mode, limit to last character } else { current_input.len() }; form_state.current_cursor_pos = event_handler .ideal_cursor_column .min(max_cursor_pos); // Don't overwrite message from handle_event if load_message is simple success if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() { event_handler.command_message = load_message; } } else { // Invalid position (e.g., 0) - reset to first entry or new entry mode app_state.current_position = 1.min(current_total_count + 1); // Go to 1 or new entry if empty if app_state.current_position > total_count { form_state.reset_to_empty(); form_state.current_field = 0; } } } else if !position_changed && !event_handler.is_edit_mode { // If position didn't change but we are in read-only, just adjust cursor let current_input = form_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 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 { let current_input = register_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_login { if !event_handler.is_edit_mode { let current_input = login_state.get_current_input(); let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } // --- End Position Change Handling --- // Check exit condition *after* all processing for the iteration if should_exit { return Ok(()); } // --- FPS Calculation --- 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(); } } // End main loop }