457 lines
21 KiB
Rust
457 lines
21 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::ui_service::UiService;
|
|
use crate::config::storage::storage::load_auth_data;
|
|
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::admin::AdminFocus;
|
|
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::tui::terminal::{EventReader, TerminalCore};
|
|
use crate::ui::handlers::render::render_ui;
|
|
use crate::tui::functions::common::login::LoginResult;
|
|
use crate::tui::functions::common::register::RegisterResult;
|
|
// Removed: use crate::tui::functions::common::add_table::handle_save_table_action;
|
|
// Removed: use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
|
|
use crate::ui::handlers::context::DialogPurpose; // UiContext removed if not used directly
|
|
use crate::tui::functions::common::login;
|
|
use crate::tui::functions::common::register;
|
|
use std::time::Instant;
|
|
use anyhow::{Context, Result};
|
|
use crossterm::cursor::SetCursorStyle;
|
|
use crossterm::event as crossterm_event;
|
|
use tracing::{error, info, warn}; // Added warn
|
|
use tokio::sync::mpsc;
|
|
|
|
|
|
pub async fn run_ui() -> Result<()> {
|
|
let config = Config::load().context("Failed to load configuration")?;
|
|
let theme = Theme::from_str(&config.colors.theme);
|
|
let mut terminal = TerminalCore::new().context("Failed to initialize terminal")?;
|
|
let mut grpc_client = GrpcClient::new().await?;
|
|
let mut command_handler = CommandHandler::new();
|
|
|
|
// --- Channel for Login Results ---
|
|
let (login_result_sender, mut login_result_receiver) =
|
|
mpsc::channel::<LoginResult>(1);
|
|
let (register_result_sender, mut register_result_receiver) =
|
|
mpsc::channel::<RegisterResult>(1);
|
|
let (save_table_result_sender, mut save_table_result_receiver) =
|
|
mpsc::channel::<Result<String>>(1);
|
|
let (save_logic_result_sender, _save_logic_result_receiver) = // Prefixed and removed mut
|
|
mpsc::channel::<Result<String>>(1);
|
|
|
|
let mut event_handler = EventHandler::new(
|
|
login_result_sender.clone(),
|
|
register_result_sender.clone(),
|
|
save_table_result_sender.clone(),
|
|
save_logic_result_sender.clone(),
|
|
).await.context("Failed to create event handler")?;
|
|
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().context("Failed to create initial app state")?;
|
|
|
|
// --- DATA: Load auth data from file at startup ---
|
|
let mut auto_logged_in = false;
|
|
match load_auth_data() {
|
|
Ok(Some(stored_data)) => {
|
|
auth_state.auth_token = Some(stored_data.access_token);
|
|
auth_state.user_id = Some(stored_data.user_id);
|
|
auth_state.role = Some(stored_data.role);
|
|
auth_state.decoded_username = Some(stored_data.username);
|
|
auto_logged_in = true;
|
|
info!("Auth data loaded from file. User is auto-logged in.");
|
|
}
|
|
Ok(None) => {
|
|
info!("No stored auth data found. User will see intro/login.");
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to load auth data: {}", e);
|
|
}
|
|
}
|
|
// --- END DATA ---
|
|
|
|
let column_names =
|
|
UiService::initialize_app_state(&mut grpc_client, &mut app_state)
|
|
.await.context("Failed to initialize app state from UI service")?;
|
|
let mut form_state = FormState::new(column_names);
|
|
|
|
UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?;
|
|
form_state.reset_to_empty();
|
|
|
|
if auto_logged_in {
|
|
buffer_state.history = vec![AppView::Form];
|
|
buffer_state.active_index = 0;
|
|
info!("Initial view set to Form due to auto-login.");
|
|
}
|
|
|
|
let mut last_frame_time = Instant::now();
|
|
let mut current_fps = 0.0;
|
|
let mut needs_redraw = true;
|
|
|
|
loop {
|
|
// --- Synchronize UI View from Active Buffer ---
|
|
if let Some(active_view) = buffer_state.get_active_view() {
|
|
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_add_logic = 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 => {
|
|
info!("Active view is Admin, refreshing profile tree...");
|
|
match grpc_client.get_profile_tree().await {
|
|
Ok(refreshed_tree) => {
|
|
app_state.profile_tree = refreshed_tree;
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to refresh profile tree for Admin panel: {}", e);
|
|
event_handler.command_message = format!("Error refreshing admin data: {}", e);
|
|
}
|
|
}
|
|
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);
|
|
|
|
if admin_state.current_focus == AdminFocus::default() ||
|
|
!matches!(admin_state.current_focus,
|
|
AdminFocus::InsideProfilesList |
|
|
AdminFocus::Tables | AdminFocus::InsideTablesList |
|
|
AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) {
|
|
admin_state.current_focus = AdminFocus::ProfilesPane;
|
|
}
|
|
if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() {
|
|
admin_state.profile_list_state.select(Some(0));
|
|
}
|
|
}
|
|
AppView::AddTable => app_state.ui.show_add_table = true,
|
|
AppView::AddLogic => app_state.ui.show_add_logic = true,
|
|
AppView::Form => app_state.ui.show_form = true,
|
|
AppView::Scratch => {}
|
|
}
|
|
}
|
|
// --- End Synchronization ---
|
|
|
|
// --- Handle Pending Table Structure Fetches ---
|
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
|
if app_state.ui.show_add_logic {
|
|
// Ensure admin_state.add_logic_state matches the pending fetch
|
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
|
admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) {
|
|
|
|
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
|
&mut grpc_client,
|
|
&mut admin_state.add_logic_state,
|
|
).await.unwrap_or_else(|e| {
|
|
error!("Error initializing add_logic_table_data: {}", e);
|
|
format!("Error fetching table structure: {}", e)
|
|
});
|
|
|
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
|
info!("{}", fetch_message);
|
|
// Optionally update command message on success if desired
|
|
// event_handler.command_message = fetch_message;
|
|
} else {
|
|
event_handler.command_message = fetch_message; // Show error/warning to user
|
|
}
|
|
needs_redraw = true;
|
|
} else {
|
|
error!(
|
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}",
|
|
profile_name, table_name,
|
|
admin_state.add_logic_state.profile_name,
|
|
admin_state.add_logic_state.selected_table_name
|
|
);
|
|
// Cleared by .take(), no need to set to None explicitly unless re-queueing
|
|
}
|
|
} else {
|
|
warn!(
|
|
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
|
|
profile_name, table_name
|
|
);
|
|
// If you need to re-queue:
|
|
// app_state.pending_table_structure_fetch = Some((profile_name, table_name));
|
|
}
|
|
}
|
|
|
|
// --- 3. Draw UI ---
|
|
if needs_redraw {
|
|
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,
|
|
&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,
|
|
);
|
|
}).context("Terminal draw call failed")?;
|
|
needs_redraw = false;
|
|
}
|
|
|
|
// --- Cursor Visibility Logic ---
|
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
|
|
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().context("Failed to show cursor in ReadOnly mode")?;
|
|
}
|
|
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().context("Failed to show cursor in Command mode")?; }
|
|
}
|
|
// --- End Cursor Visibility Logic ---
|
|
|
|
let total_count = app_state.total_count;
|
|
let mut current_position = app_state.current_position;
|
|
let position_before_event = current_position;
|
|
if app_state.ui.dialog.is_loading {
|
|
needs_redraw = true;
|
|
}
|
|
|
|
// --- 1. Handle Terminal Events ---
|
|
let mut event_outcome_result = Ok(EventOutcome::Ok(String::new()));
|
|
let mut event_processed = false;
|
|
if crossterm_event::poll(std::time::Duration::from_millis(1))? {
|
|
let event = event_reader.read_event().context("Failed to read terminal event")?;
|
|
event_processed = true;
|
|
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;
|
|
}
|
|
|
|
if event_processed {
|
|
needs_redraw = true;
|
|
}
|
|
app_state.current_position = current_position;
|
|
|
|
// --- Check for Login Results from Channel ---
|
|
match login_result_receiver.try_recv() {
|
|
Ok(result) => {
|
|
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) {
|
|
needs_redraw = true;
|
|
}
|
|
}
|
|
Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ }
|
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
|
error!("Login result channel disconnected unexpectedly.");
|
|
}
|
|
}
|
|
|
|
// --- Check for Register Results from Channel ---
|
|
match register_result_receiver.try_recv() {
|
|
Ok(result) => {
|
|
if register::handle_registration_result(result, &mut app_state, &mut register_state) {
|
|
needs_redraw = true;
|
|
}
|
|
}
|
|
Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ }
|
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
|
error!("Register result channel disconnected unexpectedly.");
|
|
}
|
|
}
|
|
// --- Check for Save Table Results ---
|
|
match save_table_result_receiver.try_recv() {
|
|
Ok(result) => {
|
|
app_state.hide_dialog();
|
|
match result {
|
|
Ok(ref success_message) => {
|
|
app_state.show_dialog(
|
|
"Save Successful",
|
|
success_message,
|
|
vec!["OK".to_string()],
|
|
DialogPurpose::SaveTableSuccess,
|
|
);
|
|
admin_state.add_table_state.has_unsaved_changes = false;
|
|
}
|
|
Err(e) => {
|
|
event_handler.command_message = format!("Save failed: {}", e);
|
|
}
|
|
}
|
|
needs_redraw = true;
|
|
}
|
|
Err(mpsc::error::TryRecvError::Empty) => {}
|
|
Err(mpsc::error::TryRecvError::Disconnected) => {
|
|
error!("Save table result channel disconnected unexpectedly.");
|
|
}
|
|
}
|
|
|
|
// --- Centralized Consequence Handling ---
|
|
let mut should_exit = false;
|
|
match event_outcome_result {
|
|
Ok(outcome) => match outcome {
|
|
EventOutcome::Ok(_message) => {
|
|
// Message is often set directly in event_handler.command_message
|
|
}
|
|
EventOutcome::Exit(message) => {
|
|
event_handler.command_message = message;
|
|
should_exit = true;
|
|
}
|
|
EventOutcome::DataSaved(save_outcome, message) => {
|
|
event_handler.command_message = message;
|
|
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: _ } => {
|
|
// Handled within event_handler or specific navigation modules
|
|
}
|
|
},
|
|
Err(e) => {
|
|
event_handler.command_message = format!("Error: {}", e);
|
|
}
|
|
}
|
|
// --- End Consequence Handling ---
|
|
|
|
// --- Position Change Handling ---
|
|
let position_changed = app_state.current_position != position_before_event;
|
|
let current_total_count = app_state.total_count; // Use current total_count
|
|
let mut position_logic_needs_redraw = false;
|
|
|
|
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 } else { 0 };
|
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
|
position_logic_needs_redraw = true;
|
|
|
|
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 {
|
|
form_state.reset_to_empty();
|
|
form_state.current_field = 0;
|
|
} else if app_state.current_position >= 1 && app_state.current_position <= current_total_count {
|
|
let current_position_to_load = app_state.current_position;
|
|
let load_message = UiService::load_adresar_by_position(
|
|
&mut grpc_client,
|
|
&mut app_state,
|
|
&mut form_state,
|
|
current_position_to_load,
|
|
)
|
|
.await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?;
|
|
|
|
let current_input_after_load = form_state.get_current_input();
|
|
let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() {
|
|
current_input_after_load.len() - 1
|
|
} else {
|
|
current_input_after_load.len()
|
|
};
|
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load);
|
|
|
|
if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() {
|
|
event_handler.command_message = load_message;
|
|
}
|
|
} else { // current_position is 0 or invalid
|
|
app_state.current_position = 1.min(current_total_count + 1);
|
|
if app_state.current_position > current_total_count { // Handles empty db case
|
|
form_state.reset_to_empty();
|
|
form_state.current_field = 0;
|
|
}
|
|
// If db is not empty, this will trigger load in next iteration if position changed to 1
|
|
}
|
|
} else 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 } 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);
|
|
}
|
|
}
|
|
|
|
if position_logic_needs_redraw {
|
|
needs_redraw = true;
|
|
}
|
|
// --- End Position Change Handling ---
|
|
|
|
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 { // Avoid division by zero
|
|
current_fps = 1.0 / frame_duration.as_secs_f64();
|
|
}
|
|
} // End main loop
|
|
}
|