From 2b37de3b4d3ba5ef484540dfd1628d7b2e9fd9ba Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 18 Apr 2025 14:44:04 +0200 Subject: [PATCH] login waiting dialog works, THIS COMMIT NEEDS TO BE REFACTORED --- Cargo.lock | 5 +- client/Cargo.toml | 1 + client/src/components/admin/add_table.rs | 13 +- client/src/components/auth/login.rs | 3 +- client/src/components/auth/register.rs | 1 + client/src/components/common/dialog.rs | 154 ++++++++++---------- client/src/modes/handlers/event.rs | 15 +- client/src/state/app/state.rs | 33 +++++ client/src/state/pages/auth.rs | 2 + client/src/tui/functions/common/login.rs | 52 ++++--- client/src/ui/handlers/ui.rs | 172 +++++++++++++---------- 11 files changed, 276 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89a4aff..37e461f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "dirs 6.0.0", "dotenvy", "lazy_static", + "log", "prost", "ratatui", "serde", @@ -1656,9 +1657,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" diff --git a/client/Cargo.toml b/client/Cargo.toml index 295607a..6cdfed7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -12,6 +12,7 @@ crossterm = "0.28.1" dirs = "6.0.0" dotenvy = "0.15.7" lazy_static = "1.5.0" +log = "0.4.27" prost = "0.13.5" ratatui = "0.29.0" serde = { version = "1.0.218", features = ["derive"] } diff --git a/client/src/components/admin/add_table.rs b/client/src/components/admin/add_table.rs index 8753e40..0d9d964 100644 --- a/client/src/components/admin/add_table.rs +++ b/client/src/components/admin/add_table.rs @@ -21,7 +21,7 @@ pub fn render_add_table( f: &mut Frame, area: Rect, theme: &Theme, - _app_state: &AppState, // Currently unused, might be needed later + app_state: &AppState, // Currently unused, might be needed later add_table_state: &mut AddTableState, is_edit_mode: bool, // Determines if canvas inputs are in edit mode highlight_state: &HighlightState, // For text highlighting in canvas @@ -486,15 +486,16 @@ pub fn render_add_table( // --- DIALOG --- // Render the dialog overlay if it's active - if _app_state.ui.dialog.dialog_show { // Use the passed-in app_state + if app_state.ui.dialog.dialog_show { // Use the passed-in app_state dialog::render_dialog( f, f.area(), // Render over the whole frame area theme, - &_app_state.ui.dialog.dialog_title, - &_app_state.ui.dialog.dialog_message, - &_app_state.ui.dialog.dialog_buttons, - _app_state.ui.dialog.dialog_active_button_index, + &app_state.ui.dialog.dialog_title, + &app_state.ui.dialog.dialog_message, + &app_state.ui.dialog.dialog_buttons, + app_state.ui.dialog.dialog_active_button_index, + app_state.ui.dialog.is_loading, ); } } diff --git a/client/src/components/auth/login.rs b/client/src/components/auth/login.rs index e4509fc..5108812 100644 --- a/client/src/components/auth/login.rs +++ b/client/src/components/auth/login.rs @@ -142,7 +142,8 @@ pub fn render_login( &app_state.ui.dialog.dialog_title, &app_state.ui.dialog.dialog_message, &app_state.ui.dialog.dialog_buttons, // Pass buttons slice - app_state.ui.dialog.dialog_active_button_index, // Pass active index + app_state.ui.dialog.dialog_active_button_index, + app_state.ui.dialog.is_loading, ); } } diff --git a/client/src/components/auth/register.rs b/client/src/components/auth/register.rs index f7dee7b..54162f6 100644 --- a/client/src/components/auth/register.rs +++ b/client/src/components/auth/register.rs @@ -168,6 +168,7 @@ pub fn render_register( &app_state.ui.dialog.dialog_message, &app_state.ui.dialog.dialog_buttons, app_state.ui.dialog.dialog_active_button_index, + app_state.ui.dialog.is_loading, ); } } diff --git a/client/src/components/common/dialog.rs b/client/src/components/common/dialog.rs index 385ae25..7333d07 100644 --- a/client/src/components/common/dialog.rs +++ b/client/src/components/common/dialog.rs @@ -18,6 +18,7 @@ pub fn render_dialog( dialog_message: &str, dialog_buttons: &[String], dialog_active_button_index: usize, + is_loading: bool, ) { // Calculate required height based on the actual number of lines in the message let message_lines: Vec<_> = dialog_message.lines().collect(); @@ -63,27 +64,36 @@ pub fn render_dialog( vertical: 1, // Top/Bottom padding inside border }); - // Layout for Message and Buttons based on actual message height - let mut constraints = vec![ - // Allocate space for message, ensuring at least 1 line height - Constraint::Length(message_height.max(1)), // Use actual calculated height - ]; - if button_row_height > 0 { - constraints.push(Constraint::Length(button_row_height)); - } + if is_loading { + // --- Loading State --- + let loading_text = Paragraph::new(dialog_message) // Use the message passed for loading + .style(Style::default().fg(theme.fg).add_modifier(Modifier::ITALIC)) + .alignment(Alignment::Center); + // Render loading message centered in the inner area + f.render_widget(loading_text, inner_area); + } else { + // --- Normal State (Message + Buttons) --- - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(constraints) - .split(inner_area); + // Layout for Message and Buttons based on actual message height + let mut constraints = vec![ + // Allocate space for message, ensuring at least 1 line height + Constraint::Length(message_height.max(1)), // Use actual calculated height + ]; + if button_row_height > 0 { + constraints.push(Constraint::Length(button_row_height)); + } - // Render Message - let available_width = inner_area.width as usize; - let ellipsis = "..."; - let ellipsis_width = UnicodeWidthStr::width(ellipsis); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(inner_area); - let processed_lines: Vec = - message_lines + // Render Message + let available_width = inner_area.width as usize; + let ellipsis = "..."; + let ellipsis_width = UnicodeWidthStr::width(ellipsis); + + let processed_lines: Vec = message_lines .into_iter() .map(|line| { let line_width = UnicodeWidthStr::width(line); @@ -91,81 +101,83 @@ pub fn render_dialog( // Truncate with ellipsis let mut truncated_len = 0; let mut current_width = 0; - // Iterate over graphemes to handle multi-byte characters correctly for (idx, grapheme) in line.grapheme_indices(true) { let grapheme_width = UnicodeWidthStr::width(grapheme); - if current_width + grapheme_width > available_width.saturating_sub(ellipsis_width) { - break; // Stop before exceeding width needed for text + ellipsis + if current_width + grapheme_width + > available_width.saturating_sub(ellipsis_width) + { + break; } current_width += grapheme_width; - truncated_len = idx + grapheme.len(); // Store the byte index of the end of the last fitting grapheme + truncated_len = idx + grapheme.len(); } - let truncated_line = format!("{}{}", &line[..truncated_len], ellipsis); - Line::from(Span::styled(truncated_line, Style::default().fg(theme.fg))) + let truncated_line = + format!("{}{}", &line[..truncated_len], ellipsis); + Line::from(Span::styled( + truncated_line, + Style::default().fg(theme.fg), + )) } else { - // Line fits, use it as is Line::from(Span::styled(line, Style::default().fg(theme.fg))) } }) - .collect(); + .collect(); - let message_paragraph = - Paragraph::new(Text::from(processed_lines)).alignment(Alignment::Center); - // Render message in the first chunk - f.render_widget(message_paragraph, chunks[0]); + let message_paragraph = + Paragraph::new(Text::from(processed_lines)).alignment(Alignment::Center); + f.render_widget(message_paragraph, chunks[0]); // Render message in the first chunk - // Render Buttons if they exist and there's a chunk for them - if !dialog_buttons.is_empty() && chunks.len() > 1 { - let button_area = chunks[1]; - let button_count = dialog_buttons.len(); + // Render Buttons if they exist and there's a chunk for them + if !dialog_buttons.is_empty() && chunks.len() > 1 { + let button_area = chunks[1]; + let button_count = dialog_buttons.len(); - // Use Ratio for potentially more even distribution with few buttons - let button_constraints = std::iter::repeat(Constraint::Ratio( - 1, - button_count as u32, - )) - .take(button_count) - .collect::>(); + let button_constraints = std::iter::repeat(Constraint::Ratio( + 1, + button_count as u32, + )) + .take(button_count) + .collect::>(); - let button_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints(button_constraints) - .horizontal_margin(1) // Add space between buttons - .split(button_area); + let button_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(button_constraints) + .horizontal_margin(1) // Add space between buttons + .split(button_area); - for (i, button_label) in dialog_buttons.iter().enumerate() { - // Ensure we don't try to render into a non-existent chunk - if i >= button_chunks.len() { - break; - } + for (i, button_label) in dialog_buttons.iter().enumerate() { + if i >= button_chunks.len() { + break; + } - let is_active = i == dialog_active_button_index; - let (button_style, border_style) = if is_active { - ( - Style::default() + let is_active = i == dialog_active_button_index; + let (button_style, border_style) = if is_active { + ( + Style::default() .fg(theme.highlight) .add_modifier(Modifier::BOLD), - Style::default().fg(theme.accent), // Highlight border - ) - } else { - ( - Style::default().fg(theme.fg), - Style::default().fg(theme.border), // Normal border - ) - }; + Style::default().fg(theme.accent), + ) + } else { + ( + Style::default().fg(theme.fg), + Style::default().fg(theme.border), + ) + }; - let button_block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(border_style); + let button_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(border_style); - f.render_widget( - Paragraph::new(button_label.as_str()) + f.render_widget( + Paragraph::new(button_label.as_str()) .block(button_block) .style(button_style) .alignment(Alignment::Center), - button_chunks[i], - ); + button_chunks[i], + ); + } } } } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index ec368e1..cb3a914 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -6,6 +6,7 @@ use crate::services::auth::AuthClient; use crate::config::binds::config::Config; use crate::ui::handlers::rat_state::UiStateHandler; use crate::ui::handlers::context::UiContext; +use crate::ui::handlers::context::DialogPurpose; use crate::functions::common::buffer; use crate::tui::{ terminal::core::TerminalCore, @@ -226,11 +227,21 @@ impl EventHandler { message = format!("Intro Option {} selected", index); } UiContext::Login => { - message = match index { - 0 => login::save(auth_state, login_state, &mut self.auth_client, app_state).await?, + let login_action_message = match index { + 0 => { // Index 0 corresponds to the "Login" button + match login::initiate_login(app_state, login_state).await { + Ok(outcome) => return Ok(outcome), + Err(e) => { + app_state.show_dialog("Error", &format!("Failed to initiate login: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); + login_state.login_request_pending = false; + "Error initiating login".to_string() + } + } + }, 1 => login::back_to_main(login_state, app_state, buffer_state).await, _ => "Invalid Login Option".to_string(), }; + message = login_action_message; } UiContext::Register => { message = match index { diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index b32c2b0..aa3d70a 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -12,6 +12,7 @@ pub struct DialogState { pub dialog_buttons: Vec, pub dialog_active_button_index: usize, pub purpose: Option, + pub is_loading: bool, } pub struct UiState { @@ -86,10 +87,41 @@ impl AppState { self.ui.dialog.dialog_buttons = buttons; self.ui.dialog.dialog_active_button_index = 0; self.ui.dialog.purpose = Some(purpose); + self.ui.dialog.is_loading = false; self.ui.dialog.dialog_show = true; self.ui.focus_outside_canvas = true; } + /// Shows a dialog specifically for loading states. + pub fn show_loading_dialog(&mut self, title: &str, message: &str) { + self.ui.dialog.dialog_title = title.to_string(); + self.ui.dialog.dialog_message = message.to_string(); + self.ui.dialog.dialog_buttons.clear(); // No buttons during loading + self.ui.dialog.dialog_active_button_index = 0; + self.ui.dialog.purpose = None; // Purpose is set when loading finishes + self.ui.dialog.is_loading = true; + self.ui.dialog.dialog_show = true; + self.ui.focus_outside_canvas = true; // Keep focus management consistent + } + + /// Updates the content of an existing dialog, typically after loading. + pub fn update_dialog_content( + &mut self, + message: &str, + buttons: Vec, + purpose: DialogPurpose, + ) { + if self.ui.dialog.dialog_show { + self.ui.dialog.dialog_message = message.to_string(); + self.ui.dialog.dialog_buttons = buttons; + self.ui.dialog.dialog_active_button_index = 0; // Reset focus + self.ui.dialog.purpose = Some(purpose); + self.ui.dialog.is_loading = false; // Loading finished + // Keep dialog_show = true + // Keep focus_outside_canvas = true + } + } + /// Hides the dialog and clears its content. pub fn hide_dialog(&mut self) { self.ui.dialog.dialog_show = false; @@ -156,6 +188,7 @@ impl Default for DialogState { dialog_buttons: Vec::new(), dialog_active_button_index: 0, purpose: None, + is_loading: false, } } } diff --git a/client/src/state/pages/auth.rs b/client/src/state/pages/auth.rs index bfca109..bc218d5 100644 --- a/client/src/state/pages/auth.rs +++ b/client/src/state/pages/auth.rs @@ -29,6 +29,7 @@ pub struct LoginState { pub current_field: usize, pub current_cursor_pos: usize, pub has_unsaved_changes: bool, + pub login_request_pending: bool, } /// Represents the state of the Registration form UI @@ -71,6 +72,7 @@ impl LoginState { current_field: 0, current_cursor_pos: 0, has_unsaved_changes: false, + login_request_pending: false, } } } diff --git a/client/src/tui/functions/common/login.rs b/client/src/tui/functions/common/login.rs index 64e2dfe..d2cedd1 100644 --- a/client/src/tui/functions/common/login.rs +++ b/client/src/tui/functions/common/login.rs @@ -7,9 +7,11 @@ use crate::state::app::state::AppState; use crate::state::app::buffer::{AppView, BufferState}; use crate::state::pages::canvas_state::CanvasState; use crate::ui::handlers::context::DialogPurpose; +use crate::modes::handlers::event::EventOutcome; // Make sure this is imported /// Attempts to log the user in using the provided credentials via gRPC. /// Updates AuthState and AppState on success or failure. +/// (This is your existing function - remains unchanged) pub async fn save( auth_state: &mut AuthState, login_state: &mut LoginState, @@ -21,66 +23,74 @@ pub async fn save( // Clear previous error/dialog state before attempting login_state.error_message = None; - // Use the helper to ensure dialog is hidden and cleared properly - app_state.hide_dialog(); + app_state.hide_dialog(); // Hide any previous dialog // Call the gRPC login method match auth_client.login(identifier, password).await { Ok(response) => { - // Store authentication details on success + // Store authentication details using correct field names auth_state.auth_token = Some(response.access_token.clone()); auth_state.user_id = Some(response.user_id.clone()); auth_state.role = Some(response.role.clone()); auth_state.decoded_username = Some(response.username.clone()); login_state.set_has_unsaved_changes(false); + login_state.error_message = None; - let success_message = format!( - "Login Successful!\n\n\ - Username: {}\n\ - User ID: {}\n\ - Role: {}", - response.username, - response.user_id, - response.role - ); + let success_message = "Login Successful!".to_string(); app_state.show_dialog( "Login Success", &success_message, - vec!["Menu".to_string(), "Exit".to_string()], + vec!["OK".to_string()], DialogPurpose::LoginSuccess, ); - + login_state.password.clear(); + login_state.current_cursor_pos = 0; Ok("Login successful, details shown in dialog.".to_string()) } Err(e) => { let error_message = format!("{}", e); - - // Use the helper method to configure and show the dialog app_state.show_dialog( "Login Failed", &error_message, vec!["OK".to_string()], DialogPurpose::LoginFailed, ); - + login_state.error_message = Some(error_message.clone()); login_state.set_has_unsaved_changes(true); - Ok(format!("Login failed: {}", error_message)) } } } +// --- Add this new function --- +/// Sets the stage for login: shows loading dialog and sets the pending flag. +/// Call this from the event handler when login is triggered. +pub async fn initiate_login( + app_state: &mut AppState, + login_state: &mut LoginState, +) -> Result> { + // Show the loading dialog immediately + app_state.show_loading_dialog("Logging In", "Please wait..."); + // Set the flag in LoginState to indicate the actual save should run next loop + login_state.login_request_pending = true; + // Return immediately to allow redraw + Ok(EventOutcome::Ok("Login initiated.".to_string())) +} +// --- End of new function --- + + /// Reverts the login form fields to empty and returns to the previous screen (Intro). pub async fn revert( login_state: &mut LoginState, - app_state: &mut AppState, + _app_state: &mut AppState, // Keep signature consistent if needed elsewhere ) -> String { // Clear the input fields login_state.username.clear(); login_state.password.clear(); login_state.error_message = None; login_state.set_has_unsaved_changes(false); + login_state.login_request_pending = false; // Ensure flag is reset on revert "Login reverted".to_string() } @@ -95,9 +105,10 @@ pub async fn back_to_main( login_state.password.clear(); login_state.error_message = None; login_state.set_has_unsaved_changes(false); + login_state.login_request_pending = false; // Ensure flag is reset // Ensure dialog is hidden if revert is called - app_state.hide_dialog(); // Uncomment if needed + app_state.hide_dialog(); // Navigation logic buffer_state.close_active_buffer(); @@ -109,3 +120,4 @@ pub async fn back_to_main( "Returned to main menu".to_string() } + diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 1aedd4d..4e42e97 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -3,6 +3,7 @@ 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}; @@ -17,11 +18,15 @@ use crate::state::pages::intro::IntroState; use crate::state::app::buffer::BufferState; use crate::state::app::buffer::AppView; use crate::state::app::state::AppState; - // Import SaveOutcome +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 log; // <-- Add log import pub async fn run_ui() -> Result<(), Box> { let config = Config::load()?; @@ -57,9 +62,6 @@ pub async fn run_ui() -> Result<(), Box> { let mut current_fps = 0.0; loop { - // Determine edit mode based on EventHandler state - let is_edit_mode = event_handler.is_edit_mode; - // --- Synchronize UI View from Active Buffer --- if let Some(active_view) = buffer_state.get_active_view() { // Reset all flags first @@ -87,6 +89,10 @@ pub async fn run_ui() -> Result<(), Box> { } // --- 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, @@ -98,7 +104,7 @@ pub async fn run_ui() -> Result<(), Box> { &mut admin_state, &buffer_state, &theme, - is_edit_mode, + event_handler.is_edit_mode, // Use event_handler's state &event_handler.highlight_state, app_state.total_count, app_state.current_position, @@ -112,77 +118,103 @@ pub async fn run_ui() -> Result<(), Box> { })?; // --- 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::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)?; - } + 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()?; + 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 --- - let total_count = app_state.total_count; // Keep track for save logic + // --- 2. Check for Pending Login Action --- + // Check *after* drawing, so the loading state was rendered. + 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) => { + let save_result = login::save( + &mut auth_state, + &mut login_state, + &mut auth_client_instance, + &mut app_state, + ).await; + match save_result { + Ok(msg) => log::info!("Login save result: {}", msg), + Err(e) => log::error!("Error during login save: {}", e), + } + } + Err(e) => { + // Handle connection error + app_state.show_dialog( + "Login Failed", + &format!("Connection Error: {}", e), + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + login_state.error_message = Some(format!("Connection Error: {}", e)); + log::error!("Failed to create AuthClient: {}", e); + } + } + // 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; - let event = event_reader.read_event()?; - - // Get the outcome from the event handler - let 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; + // --- 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 { - // Handle the Result first Ok(outcome) => match outcome { - // Handle the Ok variant containing EventOutcome EventOutcome::Ok(message) => { if !message.is_empty() { - event_handler.command_message = message; + // 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) => { @@ -191,8 +223,6 @@ pub async fn run_ui() -> Result<(), Box> { } EventOutcome::DataSaved(save_outcome, message) => { event_handler.command_message = message; // Show save status - - // *** Delegate outcome handling to UiService *** if let Err(e) = UiService::handle_save_outcome( save_outcome, &mut grpc_client, @@ -201,30 +231,26 @@ pub async fn run_ui() -> Result<(), Box> { ) .await { - // Handle potential errors from the outcome handler itself event_handler.command_message = format!("Error handling save outcome: {}", e); } - // No count update needed for UpdatedExisting or NoChange } - EventOutcome::ButtonSelected { context, index } => { - event_handler.command_message = "Internal error: Unexpected button state".to_string(); + 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) => { - // Handle errors from handle_event, e.g., log or display event_handler.command_message = format!("Error: {}", e); - // Decide if the error is fatal, maybe set should_exit = true; } - } + } // --- End Consequence Handling --- - // --- Position Change Handling (after outcome processing) --- - let position_changed = - app_state.current_position != position_before_event; // Calculate after potential update - // Recalculate total_count *after* potential update + // --- 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; - - // Handle position changes and update form state (Only when form is shown) if app_state.ui.show_form { if position_changed && !event_handler.is_edit_mode { let current_input = form_state.get_current_input(); @@ -295,7 +321,7 @@ pub async fn run_ui() -> Result<(), Box> { event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_register { - if !event_handler.is_edit_mode { + 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 @@ -305,7 +331,7 @@ pub async fn run_ui() -> Result<(), Box> { 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 { + 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 @@ -315,10 +341,10 @@ pub async fn run_ui() -> Result<(), Box> { login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } + // --- End Position Change Handling --- - // Check exit condition *after* processing outcome + // Check exit condition *after* all processing for the iteration if should_exit { - // terminal.cleanup()?; // Optional: Drop handles this return Ok(()); } @@ -329,6 +355,6 @@ pub async fn run_ui() -> Result<(), Box> { if frame_duration.as_secs_f64() > 1e-6 { current_fps = 1.0 / frame_duration.as_secs_f64(); } - } + } // End main loop }