Files
komp_ac/client/src/ui/handlers/ui.rs
2025-05-31 23:02:09 +02:00

472 lines
20 KiB
Rust

// client/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;
use crate::ui::handlers::context::DialogPurpose;
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};
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();
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) =
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")?;
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);
}
}
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 {
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 => {}
}
}
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if app_state.ui.show_add_logic {
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,
&app_state.profile_tree,
).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);
} else {
event_handler.command_message = fetch_message;
}
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
);
}
} else {
warn!(
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
profile_name, table_name
);
}
}
if needs_redraw {
terminal.draw(|f| {
render_ui(
f,
&mut form_state,
&mut auth_state,
&login_state,
&register_state,
&intro_state,
&mut admin_state,
&buffer_state,
&theme,
event_handler.is_edit_mode,
&event_handler.highlight_state,
&event_handler.command_input,
event_handler.command_mode,
&event_handler.command_message,
&event_handler.navigation_state,
app_state.total_count,
app_state.current_position,
&app_state.current_dir,
current_fps,
&app_state,
);
}).context("Terminal draw call failed")?;
needs_redraw = false;
}
if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() {
if app_state.ui.show_add_logic {
let profile_name = admin_state.add_logic_state.profile_name.clone();
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
Ok(columns) => {
admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone());
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
}
Err(e) => {
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None;
admin_state.add_logic_state.deactivate_script_editor_autocomplete();
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}
needs_redraw = true;
}
}
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")?; }
}
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;
}
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;
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) => {}
Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Login result channel disconnected unexpectedly.");
}
}
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) => {}
Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Register result channel disconnected unexpectedly.");
}
}
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.");
}
}
let mut should_exit = false;
match event_outcome_result {
Ok(outcome) => match outcome {
EventOutcome::Ok(_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: _ } => {}
},
Err(e) => {
event_handler.command_message = format!("Error: {}", e);
}
}
let position_changed = app_state.current_position != position_before_event;
let current_total_count = app_state.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 {
app_state.current_position = 1.min(current_total_count + 1);
if app_state.current_position > current_total_count {
form_state.reset_to_empty();
form_state.current_field = 0;
}
}
} 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;
}
if should_exit {
return Ok(());
}
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();
}
}
}