366 lines
16 KiB
Rust
366 lines
16 KiB
Rust
// 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<dyn std::error::Error>> {
|
|
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<String, Error>, 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
|
|
}
|
|
|