Files
komp_ac/client/src/ui/handlers/ui.rs
2025-08-30 19:26:12 +02:00

720 lines
32 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::auth::AuthState;
use crate::state::pages::auth::UserRole;
use crate::pages::login::LoginFormState;
use crate::pages::register::RegisterFormState;
use crate::pages::admin::AdminState;
use crate::pages::admin::AdminFocus;
use crate::pages::admin::admin;
use crate::pages::intro::IntroState;
use crate::pages::forms::{FormState, FieldDefinition};
use crate::pages::forms;
use crate::pages::routing::{Router, Page};
use crate::buffer::state::BufferState;
use crate::buffer::state::AppView;
use crate::state::app::state::AppState;
use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui;
use crate::pages::login;
use crate::pages::register;
use crate::pages::login::LoginResult;
use crate::pages::login::LoginState;
use crate::pages::register::RegisterResult;
use crate::ui::handlers::context::DialogPurpose;
use crate::utils::columns::filter_user_columns;
use canvas::keymap::KeyEventOutcome;
use canvas::CursorManager;
use canvas::FormEditor;
use anyhow::{Context, Result};
use crossterm::cursor::{SetCursorStyle, MoveTo};
use crossterm::event as crossterm_event;
use crossterm::ExecutableCommand;
use tracing::{error, info, warn};
use tokio::sync::mpsc;
use std::time::Instant;
use std::time::Duration;
#[cfg(feature = "ui-debug")]
use crate::state::app::state::DebugState;
#[cfg(feature = "ui-debug")]
use crate::utils::debug_logger::pop_next_debug_message;
// Rest of the file remains the same...
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.context("Failed to create GrpcClient")?;
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(),
grpc_client.clone(),
)
.await
.context("Failed to create event handler")?;
let event_reader = EventReader::new();
let mut auth_state = AuthState::default();
let mut login_state = LoginFormState::new();
login_state.editor.set_keymap(config.build_canvas_keymap());
let mut register_state = RegisterFormState::default();
register_state.editor.set_keymap(config.build_canvas_keymap());
let mut intro_state = IntroState::default();
let mut admin_state = AdminState::default();
let mut router = Router::new();
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(UserRole::from_str(&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 (initial_profile, initial_table, initial_columns_from_service) =
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
.await
.context("Failed to initialize app state and form")?;
let initial_field_defs: Vec<FieldDefinition> = filter_user_columns(initial_columns_from_service)
.into_iter()
.map(|col_name| FieldDefinition {
display_name: col_name.clone(),
data_key: col_name,
is_link: false,
link_target_table: None,
})
.collect();
// Replace local form_state with app_state.form_editor
let path = format!("{}/{}", initial_profile, initial_table);
app_state.ensure_form_editor(&path, &config, || {
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
});
buffer_state.update_history(AppView::Form(path.clone()));
router.navigate(Page::Form(path.clone()));
// Fetch initial count using app_state accessor
if let Some(form_state) = app_state.form_state_for_path(&path) {
UiService::fetch_and_set_table_count(&mut grpc_client, form_state)
.await
.context(format!(
"Failed to fetch initial count for table {}.{}",
initial_profile, initial_table
))?;
if form_state.total_count > 0 {
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
event_handler.command_message = format!("Error loading initial data: {}", e);
}
} else {
form_state.reset_to_empty();
}
}
if auto_logged_in {
let path = format!("{}/{}", initial_profile, initial_table);
buffer_state.history = vec![AppView::Form(path.clone())];
router.navigate(Page::Form(path));
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;
let mut prev_view_profile_name = app_state.current_view_profile_name.clone();
let mut prev_view_table_name = app_state.current_view_table_name.clone();
let mut table_just_switched = false;
loop {
let position_before_event = if let Page::Form(path) = &router.current {
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
} else {
1
};
let mut event_processed = false;
// --- CHANNEL RECEIVERS ---
// For main search palette
match event_handler.search_result_receiver.try_recv() {
Ok(hits) => {
info!("--- 4. Main loop received message from channel. ---");
if let Some(search_state) = app_state.search_state.as_mut() {
search_state.results = hits;
search_state.is_loading = false;
}
needs_redraw = true;
}
Err(mpsc::error::TryRecvError::Empty) => {
}
Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Search result channel disconnected!");
}
}
// --- ADDED: For live form autocomplete ---
match event_handler.autocomplete_result_receiver.try_recv() {
Ok(hits) => {
if let Page::Form(path) = &router.current {
if let Some(form_state) = app_state.form_state_for_path(path) {
if form_state.autocomplete_active {
form_state.autocomplete_suggestions = hits;
form_state.autocomplete_loading = false;
if !form_state.autocomplete_suggestions.is_empty() {
form_state.selected_suggestion_index = Some(0);
} else {
form_state.selected_suggestion_index = None;
}
event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len());
}
}
}
needs_redraw = true;
}
Err(mpsc::error::TryRecvError::Empty) => {}
Err(mpsc::error::TryRecvError::Disconnected) => {
error!("Autocomplete result channel disconnected!");
}
}
if app_state.ui.show_search_palette {
needs_redraw = true;
}
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;
// Decouple Command Line and palettes from canvas:
// Only forward keys to Form canvas when:
// - not in command mode
// - no search/palette active
// - focus is inside the canvas
if let crossterm_event::Event::Key(key_event) = &event {
let overlay_active = event_handler.command_mode
|| app_state.ui.show_search_palette
|| event_handler.navigation_state.active;
if !overlay_active {
if let Page::Form(path) = &router.current {
if !app_state.ui.focus_outside_canvas {
if let Some(editor) = app_state.editor_for_path(path) {
match editor.handle_key_event(*key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
event_handler.command_message = msg;
needs_redraw = true;
continue;
}
KeyEventOutcome::Consumed(None) => {
needs_redraw = true;
continue;
}
KeyEventOutcome::Pending => {
needs_redraw = true;
continue;
}
KeyEventOutcome::NotMatched => {
// fall through to client-level handling
}
}
}
}
}
}
}
// Call handle_event directly
let event_outcome_result = event_handler.handle_event(
event,
&config,
&mut terminal,
&mut command_handler,
&mut auth_state,
&mut buffer_state,
&mut app_state,
&mut router,
).await;
let mut should_exit = false;
match event_outcome_result {
Ok(outcome) => match outcome {
EventOutcome::Ok(message) => {
if !message.is_empty() {
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;
if let Page::Form(path) = &router.current {
if let Some(mut temp_form_state) = app_state.form_state_for_path(path).cloned() {
if let Err(e) = UiService::handle_save_outcome(
save_outcome,
&mut grpc_client,
&mut app_state,
&mut temp_form_state,
).await {
event_handler.command_message =
format!("Error handling save outcome: {}", e);
}
// Update app_state with changes
if let Some(form_state) = app_state.form_state_for_path(path) {
*form_state = temp_form_state;
}
}
}
}
EventOutcome::ButtonSelected { .. } => {}
EventOutcome::TableSelected { path } => {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() == 2 {
let profile_name = parts[0].to_string();
let table_name = parts[1].to_string();
app_state.set_current_view_table(profile_name, table_name);
buffer_state.update_history(AppView::Form(path.clone()));
event_handler.command_message = format!("Loading table: {}", path);
} else {
event_handler.command_message = format!("Invalid table path: {}", path);
}
}
},
Err(e) => {
event_handler.command_message = format!("Error: {}", e);
}
}
if should_exit {
return Ok(());
}
}
match login_result_receiver.try_recv() {
Ok(result) => {
// Apply result to the active router Login page if present,
// otherwise update the local copy.
let updated = if let Page::Login(page) = &mut router.current {
login::handle_login_result(
result,
&mut app_state,
&mut auth_state,
page,
)
} else {
login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state)
};
if updated { 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.");
}
}
if let Some(active_view) = buffer_state.get_active_view() {
match active_view {
AppView::Intro => {
// Keep external intro_state in sync with the live Router state
if let Page::Intro(current) = &router.current {
intro_state = current.clone();
}
// Navigate with the up-to-date state
router.navigate(Page::Intro(intro_state.clone()));
}
AppView::Login => {
// Do not re-create the page every frame. If we're already on Login,
// keep it. If we just switched into Login, create it once and
// inject the keymap.
if let Page::Login(_) = &router.current {
// Already on login page; keep existing state
} else {
let mut page = LoginFormState::new();
page.editor.set_keymap(config.build_canvas_keymap());
router.navigate(Page::Login(page));
}
}
AppView::Register => {
if let Page::Register(_) = &router.current {
// already on register page
} else {
let mut page = RegisterFormState::new();
page.editor.set_keymap(config.build_canvas_keymap());
router.navigate(Page::Register(page));
}
}
AppView::Admin => {
if let Page::Admin(current) = &router.current {
admin_state = current.clone();
}
info!("Auth role at render: {:?}", auth_state.role);
// Use the admin loader instead of inline logic
if let Err(e) = admin::loader::refresh_admin_state(&mut grpc_client, &mut app_state, &mut admin_state).await {
error!("Failed to refresh admin state: {}", e);
event_handler.command_message = format!("Error refreshing admin data: {}", e);
}
router.navigate(Page::Admin(admin_state.clone()));
}
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
AppView::Form(path) => {
// Keep current_view_* consistent with the active buffer path
if let Some((profile, table)) = path.split_once('/') {
app_state.set_current_view_table(
profile.to_string(),
table.to_string(),
);
}
router.navigate(Page::Form(path.clone()));
}
AppView::Scratch => {}
}
}
if let Page::Form(_current_path) = &router.current {
let current_view_profile = app_state.current_view_profile_name.clone();
let current_view_table = app_state.current_view_table_name.clone();
if prev_view_profile_name != current_view_profile
|| prev_view_table_name != current_view_table
{
if let (Some(prof_name), Some(tbl_name)) =
(current_view_profile.as_ref(), current_view_table.as_ref())
{
app_state.show_loading_dialog(
"Loading Table",
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
);
needs_redraw = true;
// DELEGATE to the forms loader
match forms::loader::ensure_form_loaded_and_count(
&mut grpc_client,
&mut app_state,
&config,
prof_name,
tbl_name,
).await {
Ok(()) => {
app_state.hide_dialog();
prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table;
table_just_switched = true;
}
Err(e) => {
app_state.update_dialog_content(
&format!("Error loading table: {}", e),
vec!["OK".to_string()],
DialogPurpose::LoginFailed,
);
// Reset to previous state on error
app_state.current_view_profile_name = prev_view_profile_name.clone();
app_state.current_view_table_name = prev_view_table_name.clone();
}
}
}
needs_redraw = true;
}
}
// Continue with the rest of the positioning logic...
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if let Page::AddLogic(state) = &mut router.current {
if state.profile_name == profile_name
&& 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,
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 AddLogic state is for {}.{:?}",
profile_name,
table_name,
state.profile_name,
state.selected_table_name
);
}
} else {
warn!(
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
profile_name, table_name
);
}
}
if let Page::AddLogic(state) = &mut router.current {
if let Some(table_name) = state.script_editor_awaiting_column_autocomplete.clone() {
let profile_name = 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) => {
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);
state.script_editor_awaiting_column_autocomplete = None;
state.deactivate_script_editor_autocomplete();
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
}
}
needs_redraw = true;
}
}
let current_position = if let Page::Form(path) = &router.current {
app_state.form_state_for_path(path).map(|fs| fs.current_position).unwrap_or(1)
} else {
1
};
let position_changed = current_position != position_before_event;
let mut position_logic_needs_redraw = false;
if let Page::Form(path) = &router.current {
if !table_just_switched {
if position_changed && !app_state.is_canvas_edit_mode_at(path) {
position_logic_needs_redraw = true;
if let Some(form_state) = app_state.form_state_for_path(path) {
if form_state.current_position > form_state.total_count {
form_state.reset_to_empty();
event_handler.command_message = format!(
"New entry for {}.{}",
form_state.profile_name,
form_state.table_name
);
} else {
match UiService::load_table_data_by_position(&mut grpc_client, form_state).await {
Ok(load_message) => {
if event_handler.command_message.is_empty()
|| !load_message.starts_with("Error")
{
event_handler.command_message = load_message;
}
}
Err(e) => {
event_handler.command_message =
format!("Error loading data: {}", e);
}
}
}
let current_input_after_load_str = form_state.get_current_input();
let current_input_len_after_load =
current_input_after_load_str.chars().count();
let max_cursor_pos = if current_input_len_after_load > 0 {
current_input_len_after_load.saturating_sub(1)
} else {
0
};
form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
}
} else if !position_changed && !app_state.is_canvas_edit_mode_at(path) {
if let Some(form_state) = app_state.form_state_for_path(path) {
let current_input_str = form_state.get_current_input();
let current_input_len = current_input_str.chars().count();
let max_cursor_pos = if current_input_len > 0 {
current_input_len.saturating_sub(1)
} else {
0
};
form_state.current_cursor_pos =
event_handler.ideal_cursor_column.min(max_cursor_pos);
}
}
}
} else if let Page::Register(state) = &mut router.current {
if !app_state.is_canvas_edit_mode() {
let current_input = state.get_current_input();
let max_cursor_pos =
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos));
}
} else if let Page::Login(state) = &mut router.current {
if !app_state.is_canvas_edit_mode() {
let current_input = state.get_current_input();
let max_cursor_pos =
if !current_input.is_empty() { current_input.len() - 1 } else { 0 };
state.set_current_cursor_pos(event_handler.ideal_cursor_column.min(max_cursor_pos));
}
}
if position_logic_needs_redraw {
needs_redraw = true;
}
if app_state.ui.dialog.is_loading {
needs_redraw = true;
}
#[cfg(feature = "ui-debug")]
{
let can_display_next = match &app_state.debug_state {
Some(current) => current.display_start_time.elapsed() >= Duration::from_secs(2),
None => true,
};
if can_display_next {
if let Some((new_message, is_error)) = pop_next_debug_message() {
app_state.debug_state = Some(DebugState {
displayed_message: new_message,
is_error,
display_start_time: Instant::now(),
});
}
}
}
if event_processed || needs_redraw || position_changed {
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
match current_mode {
AppMode::General => {
if app_state.ui.focus_outside_canvas {
// Outside canvas → app decides
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
terminal.show_cursor()?;
} else {
// Inside canvas → let canvas handle it
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
let _ = CursorManager::update_for_mode(editor.mode());
}
}
if let Page::Login(page) = &router.current {
let _ = CursorManager::update_for_mode(page.editor.mode());
}
if let Page::Register(page) = &router.current {
let _ = CursorManager::update_for_mode(page.editor.mode());
}
}
}
AppMode::Command => {
// Command line overlay → app decides
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
terminal.show_cursor()?;
}
}
let current_dir = app_state.current_dir.clone();
terminal
.draw(|f| {
render_ui(
f,
&mut router,
&buffer_state,
&theme,
&event_handler.command_input,
event_handler.command_mode,
&event_handler.command_message,
&event_handler.navigation_state,
&current_dir,
current_fps,
&app_state,
&auth_state,
);
})
.context("Terminal draw call failed")?;
needs_redraw = false;
}
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();
}
table_just_switched = false;
}
}