login waiting dialog works, THIS COMMIT NEEDS TO BE REFACTORED
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user