// src/modes/handlers/event.rs use crate::config::binds::config::Config; use crate::config::binds::key_sequences::KeySequenceTracker; use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list}; use crate::sidebar::toggle_sidebar; use crate::search::event::handle_search_palette_event; use crate::pages::admin_panel::add_logic; use crate::pages::admin_panel::add_table; use crate::pages::register::suggestions::RoleSuggestionsProvider; use crate::pages::admin::main::logic::handle_admin_navigation; use crate::pages::admin::admin; use crate::modes::general::command_navigation::{ handle_command_navigation_event, NavigationState, }; use crate::modes::{ common::{command_mode, commands::CommandHandler}, general::navigation, handlers::mode_manager::{AppMode, ModeManager}, }; use crate::services::auth::AuthClient; use crate::services::grpc_client::GrpcClient; use canvas::AppMode as CanvasMode; use canvas::DataProvider; use crate::state::app::state::AppState; use crate::pages::admin::AdminState; use crate::state::pages::auth::AuthState; use crate::state::pages::auth::UserRole; use crate::pages::login::LoginState; use crate::pages::register::RegisterState; use crate::pages::intro::IntroState; use crate::pages::login; use crate::pages::register; use crate::pages::intro; use crate::pages::login::logic::LoginResult; use crate::pages::register::RegisterResult; use crate::pages::routing::{Router, Page}; use crate::movement::MovementAction; use crate::dialog; use crate::pages::forms; use crate::pages::forms::FormState; use crate::pages::forms::logic::{save, revert, SaveOutcome}; use crate::search::state::SearchState; use crate::tui::{ terminal::core::TerminalCore, }; use crate::ui::handlers::context::UiContext; use canvas::KeyEventOutcome; use canvas::SuggestionsProvider; use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; use crossterm::event::{Event, KeyCode}; use tokio::sync::mpsc; use tokio::sync::mpsc::unbounded_channel; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { Ok(String), Exit(String), DataSaved(SaveOutcome, String), ButtonSelected { context: UiContext, index: usize }, TableSelected { path: String }, } impl EventOutcome { pub fn get_message_if_ok(&self) -> String { match self { EventOutcome::Ok(msg) => msg.clone(), _ => String::new(), } } } pub struct EventHandler { pub command_mode: bool, pub command_input: String, pub command_message: String, pub edit_mode_cooldown: bool, pub ideal_cursor_column: usize, pub key_sequence_tracker: KeySequenceTracker, pub auth_client: AuthClient, pub grpc_client: GrpcClient, pub login_result_sender: mpsc::Sender, pub register_result_sender: mpsc::Sender, pub save_table_result_sender: add_table::nav::SaveTableResultSender, pub save_logic_result_sender: add_logic::nav::SaveLogicResultSender, pub navigation_state: NavigationState, pub search_result_sender: mpsc::UnboundedSender>, pub search_result_receiver: mpsc::UnboundedReceiver>, pub autocomplete_result_sender: mpsc::UnboundedSender>, pub autocomplete_result_receiver: mpsc::UnboundedReceiver>, } impl EventHandler { pub async fn new( login_result_sender: mpsc::Sender, register_result_sender: mpsc::Sender, save_table_result_sender: add_table::nav::SaveTableResultSender, save_logic_result_sender: add_logic::nav::SaveLogicResultSender, grpc_client: GrpcClient, ) -> Result { let (search_tx, search_rx) = unbounded_channel(); let (autocomplete_tx, autocomplete_rx) = unbounded_channel(); Ok(EventHandler { command_mode: false, command_input: String::new(), command_message: String::new(), edit_mode_cooldown: false, ideal_cursor_column: 0, key_sequence_tracker: KeySequenceTracker::new(400), auth_client: AuthClient::new().await?, grpc_client, login_result_sender, register_result_sender, save_table_result_sender, save_logic_result_sender, navigation_state: NavigationState::new(), search_result_sender: search_tx, search_result_receiver: search_rx, autocomplete_result_sender: autocomplete_tx, autocomplete_result_receiver: autocomplete_rx, }) } pub fn is_navigation_active(&self) -> bool { self.navigation_state.active } pub fn activate_find_file(&mut self, options: Vec) { self.navigation_state.activate_find_file(options); } // Helper functions - replace the removed event_helper functions fn get_current_field_for_state(&self, router: &Router, app_state: &AppState) -> usize { match &router.current { Page::Login(state) => state.current_field(), Page::Register(state) => state.current_field(), Page::Form(path) => app_state .editor_for_path_ref(path) .map(|e| e.data_provider().current_field()) .unwrap_or(0), _ => 0, } } fn get_current_cursor_pos_for_state(&self, router: &Router, app_state: &AppState) -> usize { match &router.current { Page::Login(state) => state.current_cursor_pos(), Page::Register(state) => state.current_cursor_pos(), Page::Form(path) => app_state .form_state_for_path_ref(path) .map(|fs| fs.current_cursor_pos()) .unwrap_or(0), _ => 0, } } fn get_has_unsaved_changes_for_state(&self, router: &Router, app_state: &AppState) -> bool { match &router.current { Page::Login(state) => state.has_unsaved_changes(), Page::Register(state) => state.has_unsaved_changes(), Page::Form(path) => app_state .form_state_for_path_ref(path) .map(|fs| fs.has_unsaved_changes()) .unwrap_or(false), _ => false, } } fn get_current_input_for_state<'a>( &'a self, router: &'a Router, app_state: &'a AppState, ) -> &'a str { match &router.current { Page::Login(state) => state.get_current_input(), Page::Register(state) => state.get_current_input(), Page::Form(path) => app_state .form_state_for_path_ref(path) .map(|fs| fs.get_current_input()) .unwrap_or(""), _ => "", } } fn set_current_cursor_pos_for_state( &mut self, router: &mut Router, app_state: &mut AppState, pos: usize, ) { match &mut router.current { Page::Login(state) => state.set_current_cursor_pos(pos), Page::Register(state) => state.set_current_cursor_pos(pos), Page::Form(path) => { if let Some(fs) = app_state.form_state_for_path(path) { fs.set_current_cursor_pos(pos); } } _ => {}, } } fn get_cursor_pos_for_mixed_state(&self, router: &Router, app_state: &AppState) -> usize { match &router.current { Page::Login(state) => state.current_cursor_pos(), Page::Register(state) => state.current_cursor_pos(), Page::Form(path) => app_state .form_state_for_path_ref(path) .map(|fs| fs.current_cursor_pos()) .unwrap_or(0), _ => 0, } } #[allow(clippy::too_many_arguments)] pub async fn handle_event( &mut self, event: Event, config: &Config, terminal: &mut TerminalCore, command_handler: &mut CommandHandler, auth_state: &mut AuthState, buffer_state: &mut BufferState, app_state: &mut AppState, router: &mut Router, ) -> Result { if app_state.ui.show_search_palette { if let Event::Key(key_event) = event { if let Some(message) = handle_search_palette_event( key_event, app_state, &mut self.grpc_client, self.search_result_sender.clone(), ).await? { return Ok(EventOutcome::Ok(message)); } return Ok(EventOutcome::Ok(String::new())); } } let mut current_mode = ModeManager::derive_mode(app_state, self, router); if current_mode == AppMode::General && self.navigation_state.active { if let Event::Key(key_event) = event { let outcome = handle_command_navigation_event( &mut self.navigation_state, key_event, config, ) .await?; if !self.navigation_state.active { self.command_message = outcome.get_message_if_ok(); current_mode = ModeManager::derive_mode(app_state, self, router); } app_state.update_mode(current_mode); return Ok(outcome); } app_state.update_mode(current_mode); return Ok(EventOutcome::Ok(String::new())); } app_state.update_mode(current_mode); let current_view = match &router.current { Page::Intro(_) => AppView::Intro, Page::Login(_) => AppView::Login, Page::Register(_) => AppView::Register, Page::Admin(_) => AppView::Admin, Page::AddLogic(_) => AppView::AddLogic, Page::AddTable(_) => AppView::AddTable, Page::Form(path) => AppView::Form(path.clone()), }; buffer_state.update_history(current_view); if app_state.ui.dialog.dialog_show { if let Event::Key(key_event) = event { if let Some(dialog_result) = dialog::handle_dialog_event( &Event::Key(key_event), config, app_state, buffer_state, router, ) .await { return dialog_result; } } else if let Event::Resize(_, _) = event { } return Ok(EventOutcome::Ok(String::new())); } if let Event::Key(key_event) = event { let key_code = key_event.code; let modifiers = key_event.modifiers; // LOGIN: canvas <-> buttons focus handoff // Do not let Login canvas receive keys when overlays/palettes are active let overlay_active = self.command_mode || app_state.ui.show_search_palette || self.navigation_state.active; if !overlay_active { if let Page::Login(login_page) = &mut router.current { let outcome = login::event::handle_login_event(event, app_state, login_page)?; // Only return if the login page actually consumed the key if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } } else if let Page::Register(register_page) = &mut router.current { let outcome = crate::pages::register::event::handle_register_event( event, app_state, register_page, )?; // Only stop if page actually consumed the key; else fall through to global handling if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } } else if let Page::Form(path) = &router.current { let outcome = forms::event::handle_form_event( event, app_state, path, &mut self.ideal_cursor_column, )?; // Only return if the form page actually consumed the key if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } } else if let Page::Admin(admin_state) = &mut router.current { if matches!(auth_state.role, Some(UserRole::Admin)) { if let Event::Key(key_event) = event { if admin::event::handle_admin_event( key_event, config, app_state, admin_state, buffer_state, &mut self.command_message, )? { return Ok(EventOutcome::Ok(self.command_message.clone())); } } } } } if toggle_sidebar( &mut app_state.ui, config, key_code, modifiers, ) { let message = format!( "Sidebar {}", if app_state.ui.show_sidebar { "shown" } else { "hidden" } ); return Ok(EventOutcome::Ok(message)); } if toggle_buffer_list( &mut app_state.ui, config, key_code, modifiers, ) { let message = format!( "Buffer {}", if app_state.ui.show_buffer_list { "shown" } else { "hidden" } ); return Ok(EventOutcome::Ok(message)); } if current_mode == AppMode::General { if let Some(action) = config.get_action_for_key_in_mode( &config.keybindings.global, key_code, modifiers, ) { match action { "next_buffer" => { if switch_buffer(buffer_state, true) { return Ok(EventOutcome::Ok( "Switched to next buffer".to_string(), )); } } "previous_buffer" => { if switch_buffer(buffer_state, false) { return Ok(EventOutcome::Ok( "Switched to previous buffer".to_string(), )); } } "close_buffer" => { let current_table_name = app_state.current_view_table_name.as_deref(); let message = buffer_state .close_buffer_with_intro_fallback( current_table_name, ); return Ok(EventOutcome::Ok(message)); } _ => {} } } if let Some(action) = config.get_general_action(key_code, modifiers) { if action == "open_search" { if let Page::Form(_) = &router.current { if let Some(table_name) = app_state.current_view_table_name.clone() { app_state.ui.show_search_palette = true; app_state.search_state = Some(SearchState::new(table_name)); app_state.ui.focus_outside_canvas = true; return Ok(EventOutcome::Ok( "Search palette opened".to_string(), )); } } } } } match current_mode { AppMode::General => { // Map keys to MovementAction let movement_action = if let Some(act) = config.get_general_action(key_event.code, key_event.modifiers) { use crate::movement::MovementAction; match act { "up" => Some(MovementAction::Up), "down" => Some(MovementAction::Down), "left" => Some(MovementAction::Left), "right" => Some(MovementAction::Right), "next" => Some(MovementAction::Next), "previous" => Some(MovementAction::Previous), "select" => Some(MovementAction::Select), "esc" => Some(MovementAction::Esc), _ => None, } } else { None }; // Let the current page handle decoupled movement first if let Some(ma) = movement_action { match &mut router.current { Page::Intro(state) => { if state.handle_movement(ma) { return Ok(EventOutcome::Ok(String::new())); } } _ => {} } } // Optional page-specific handlers (non-movement or rich actions) let client_clone = self.grpc_client.clone(); let sender_clone = self.save_logic_result_sender.clone(); if add_logic::nav::handle_add_logic_navigation( key_event, config, app_state, buffer_state, client_clone, sender_clone, &mut self.command_message, router, ) { return Ok(EventOutcome::Ok( self.command_message.clone(), )); } if let Page::AddTable(add_table_state) = &mut router.current { let client_clone = self.grpc_client.clone(); let sender_clone = self.save_table_result_sender.clone(); if crate::pages::admin_panel::add_table::event::handle_add_table_event( key_event, movement_action, // wrapper handles both movement + nav config, app_state, add_table_state, client_clone, sender_clone, &mut self.command_message, ) { return Ok(EventOutcome::Ok(self.command_message.clone())); } } // Generic navigation for the rest (Intro/Login/Register/Form) let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) { // Skip generic navigation for AddTable/AddLogic (they have their own handlers) Ok(EventOutcome::Ok(String::new())) } else { navigation::handle_navigation_event( key_event, config, app_state, router, &mut self.command_mode, &mut self.command_input, &mut self.command_message, &mut self.navigation_state, ).await }; match nav_outcome { Ok(EventOutcome::ButtonSelected { context, index }) => { let message = match context { UiContext::Intro => { intro::handle_intro_selection( app_state, buffer_state, index, ); if let Page::Admin(admin_state) = &mut router.current { if !app_state.profile_tree.profiles.is_empty() { admin_state.profile_list_state.select(Some(0)); } } format!("Intro Option {} selected", index) } UiContext::Login => { if let Page::Login(login_state) = &mut router.current { match index { 0 => login::initiate_login( login_state, app_state, self.auth_client.clone(), self.login_result_sender.clone(), ), 1 => login::back_to_main( login_state, app_state, buffer_state, ) .await, _ => "Invalid Login Option".to_string(), } } else { "Invalid state".to_string() } } UiContext::Register => { if let Page::Register(register_state) = &mut router.current { match index { 0 => register::initiate_registration( register_state, app_state, self.auth_client.clone(), self.register_result_sender.clone(), ), 1 => register::back_to_login( register_state, app_state, buffer_state, ) .await, _ => "Invalid Login Option".to_string(), } } else { "Invalid state".to_string() } } UiContext::Admin => { if let Page::Admin(admin_state) = &router.current { admin::tui::handle_admin_selection( app_state, admin_state, ); } format!("Admin Option {} selected", index) } UiContext::Dialog => { "Internal error: Unexpected dialog state".to_string() } }; return Ok(EventOutcome::Ok(message)); } other => return other, } } AppMode::Command => { if config.is_exit_command_mode(key_code, modifiers) { self.command_input.clear(); self.command_message.clear(); self.command_mode = false; self.key_sequence_tracker.reset(); if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { editor.set_mode(CanvasMode::ReadOnly); } } return Ok(EventOutcome::Ok( "Exited command mode".to_string(), )); } if config.is_command_execute(key_code, modifiers) { let (mut current_position, total_count) = if let Page::Form(path) = &router.current { if let Some(fs) = app_state.form_state_for_path_ref(path) { (fs.current_position, fs.total_count) } else { (1, 0) } } else { (1, 0) }; let outcome = command_mode::handle_command_event( key_event, config, app_state, router, &mut self.command_input, &mut self.command_message, &mut self.grpc_client, command_handler, terminal, &mut current_position, total_count, ).await?; if let Page::Form(path) = &router.current { if let Some(fs) = app_state.form_state_for_path(path) { fs.current_position = current_position; } } self.command_mode = false; self.key_sequence_tracker.reset(); let new_mode = ModeManager::derive_mode( app_state, self, router, ); app_state.update_mode(new_mode); return Ok(outcome); } if key_code == KeyCode::Backspace { self.command_input.pop(); self.key_sequence_tracker.reset(); return Ok(EventOutcome::Ok(String::new())); } if let KeyCode::Char(c) = key_code { if c == 'f' { self.key_sequence_tracker.add_key(key_code); let sequence = self.key_sequence_tracker.get_sequence(); if config.matches_key_sequence_generalized( &sequence, ) == Some("find_file_palette_toggle") { if matches!(&router.current, Page::Form(_) | Page::Intro(_)) { let mut all_table_paths: Vec = app_state .profile_tree .profiles .iter() .flat_map(|profile| { profile.tables.iter().map( move |table| { format!( "{}/{}", profile.name, table.name ) }, ) }) .collect(); all_table_paths.sort(); self.navigation_state .activate_find_file(all_table_paths); self.command_mode = false; self.command_input.clear(); self.command_message.clear(); self.key_sequence_tracker.reset(); return Ok(EventOutcome::Ok( "Table selection palette activated" .to_string(), )); } else { self.key_sequence_tracker.reset(); self.command_input.push('f'); if sequence.len() > 1 && sequence[0] == KeyCode::Char('f') { self.command_input.push('f'); } self.command_message = "Find File not available in this view." .to_string(); return Ok(EventOutcome::Ok( self.command_message.clone(), )); } } if config.is_key_sequence_prefix(&sequence) { return Ok(EventOutcome::Ok(String::new())); } } if c != 'f' && !self.key_sequence_tracker.current_sequence.is_empty() { self.key_sequence_tracker.reset(); } self.command_input.push(c); return Ok(EventOutcome::Ok(String::new())); } self.key_sequence_tracker.reset(); return Ok(EventOutcome::Ok(String::new())); } } } else if let Event::Resize(_, _) = event { return Ok(EventOutcome::Ok("Resized".to_string())); } self.edit_mode_cooldown = false; Ok(EventOutcome::Ok(self.command_message.clone())) } fn is_processed_command(&self, command: &str) -> bool { matches!(command, "w" | "q" | "q!" | "wq" | "r") } async fn handle_core_action( &mut self, action: &str, auth_state: &mut AuthState, terminal: &mut TerminalCore, app_state: &mut AppState, router: &mut Router, ) -> Result { match action { "save" => { if let Page::Login(login_state) = &mut router.current { let message = login::logic::save( auth_state, login_state, &mut self.auth_client, app_state, ) .await?; Ok(EventOutcome::Ok(message)) } else { if let Page::Form(path) = &router.current { forms::event::save_form(app_state, path, &mut self.grpc_client).await } else { Ok(EventOutcome::Ok("Nothing to save".to_string())) } } } "force_quit" => { if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { editor.cleanup_cursor()?; } } terminal.cleanup()?; Ok(EventOutcome::Exit( "Force exiting without saving.".to_string(), )) } "save_and_quit" => { let message = if let Page::Login(login_state) = &mut router.current { login::logic::save( auth_state, login_state, &mut self.auth_client, app_state, ) .await? } else if let Page::Form(path) = &router.current { let save_result = forms::event::save_form(app_state, path, &mut self.grpc_client).await?; match save_result { EventOutcome::DataSaved(_, msg) => msg, EventOutcome::Ok(msg) => msg, _ => "Saved".to_string(), } } else { "No changes to save.".to_string() }; if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { editor.cleanup_cursor()?; } } terminal.cleanup()?; Ok(EventOutcome::Exit(format!( "{}. Exiting application.", message ))) } "revert" => { let message = if let Page::Login(login_state) = &mut router.current { login::logic::revert(login_state, app_state).await } else if let Page::Register(register_state) = &mut router.current { register::revert( register_state, app_state, ) .await } else { if let Page::Form(path) = &router.current { return forms::event::revert_form(app_state, path, &mut self.grpc_client).await; } else { "Nothing to revert".to_string() } }; Ok(EventOutcome::Ok(message)) } _ => Ok(EventOutcome::Ok(format!( "Core action not handled: {}", action ))), } } fn is_mode_transition_action(action: &str) -> bool { matches!(action, "exit" | "exit_edit_mode" | "enter_edit_mode_before" | "enter_edit_mode_after" | "enter_command_mode" | "exit_command_mode" | "enter_highlight_mode" | "enter_highlight_mode_linewise" | "exit_highlight_mode" | "save" | "quit" | "Force_quit" | "save_and_quit" | "revert" | "enter_decider" | "trigger_autocomplete" | "suggestion_up" | "suggestion_down" | "previous_entry" | "next_entry" | "toggle_sidebar" | "toggle_buffer_list" | "next_buffer" | "previous_buffer" | "close_buffer" | "open_search" | "find_file_palette_toggle" ) } }