login waiting dialog works, THIS COMMIT NEEDS TO BE REFACTORED

This commit is contained in:
filipriec
2025-04-18 14:44:04 +02:00
parent 73d9a6367c
commit 2b37de3b4d
11 changed files with 276 additions and 175 deletions

5
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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<Line> =
message_lines
// Render Message
let available_width = inner_area.width as usize;
let ellipsis = "...";
let ellipsis_width = UnicodeWidthStr::width(ellipsis);
let processed_lines: Vec<Line> = 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::<Vec<_>>();
let button_constraints = std::iter::repeat(Constraint::Ratio(
1,
button_count as u32,
))
.take(button_count)
.collect::<Vec<_>>();
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],
);
}
}
}
}

View File

@@ -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 {

View File

@@ -12,6 +12,7 @@ pub struct DialogState {
pub dialog_buttons: Vec<String>,
pub dialog_active_button_index: usize,
pub purpose: Option<DialogPurpose>,
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<String>,
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,
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -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<EventOutcome, Box<dyn std::error::Error>> {
// 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()
}

View File

@@ -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<dyn std::error::Error>> {
let config = Config::load()?;
@@ -57,9 +62,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
}
// --- 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<dyn std::error::Error>> {
&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<dyn std::error::Error>> {
})?;
// --- 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<dyn std::error::Error>> {
}
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<dyn std::error::Error>> {
)
.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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
if frame_duration.as_secs_f64() > 1e-6 {
current_fps = 1.0 / frame_duration.as_secs_f64();
}
}
} // End main loop
}