// src/modes/handlers/event.rs use crate::config::binds::config::Config; use crate::input::engine::{InputContext, InputEngine, InputOutcome}; use crate::input::action::{AppAction, BufferAction, CoreAction}; 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; use tracing::info; #[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 input_engine: InputEngine, 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, input_engine: InputEngine::new(400, 5000), 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 { info!( "RAW KEY: code={:?} mods={:?} active_seq={} ", key_event.code, key_event.modifiers, self.input_engine.has_active_sequence(), ); 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; info!( "RAW KEY: code={:?} mods={:?} pre_active_seq={}", key_code, modifiers, self.input_engine.has_active_sequence() ); let overlay_active = self.command_mode || app_state.ui.show_search_palette || self.navigation_state.active; // Determine if canvas is in edit mode (we avoid capturing navigation then) let in_form_edit_mode = matches!( &router.current, Page::Form(path) if { if let Some(editor) = app_state.editor_for_path_ref(path) { editor.mode() == CanvasMode::Edit } else { false } } ); // Centralized key -> action resolution let allow_nav = self.input_engine.has_active_sequence() || (!in_form_edit_mode && !overlay_active); let input_ctx = InputContext { app_mode: current_mode, overlay_active, allow_navigation_capture: allow_nav, }; info!( "InputContext: app_mode={:?}, overlay_active={}, in_form_edit_mode={}, allow_nav={}, has_active_seq={}", current_mode, overlay_active, in_form_edit_mode, allow_nav, self.input_engine.has_active_sequence() ); let outcome = self.input_engine.process_key(key_event, &input_ctx, config); info!( "ENGINE OUTCOME: {:?} post_active_seq={}", outcome, self.input_engine.has_active_sequence() ); match outcome { InputOutcome::Action(action) => { if let Some(outcome) = self .handle_app_action( action, key_event, // pass original key config, terminal, command_handler, auth_state, buffer_state, app_state, router, ) .await? { return Ok(outcome); } // No early return on None (e.g., Navigate) — fall through } InputOutcome::Pending => { // waiting for more keys in a sequence return Ok(EventOutcome::Ok(String::new())); } InputOutcome::PassThrough => { // fall through to page/canvas handlers } } // LOGIN: canvas <-> buttons focus handoff // Do not let Login canvas receive keys when overlays/palettes are active if !overlay_active { if let Page::Login(login_page) = &mut router.current { let outcome = login::event::handle_login_event(event.clone(), 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_str) = &router.current { let path = path_str.clone(); if let Event::Key(_key_event) = event { // Do NOT call the input engine here again. The top-level // process_key call above already ran for this key. // If we are waiting for more leader keys, swallow the key. if self.input_engine.has_active_sequence() { return Ok(EventOutcome::Ok(String::new())); } // Otherwise, forward to the form editor/canvas. let outcome = forms::event::handle_form_event( event, app_state, &path, &mut self.ideal_cursor_column, )?; if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } } } else if let Page::AddLogic(add_logic_page) = &mut router.current { // Allow ":" (enter_command_mode) even when inside AddLogic canvas if let Some(action) = config.get_general_action(key_event.code, key_event.modifiers) { if action == "enter_command_mode" && !self.command_mode && !app_state.ui.show_search_palette && !self.navigation_state.active { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); self.input_engine.reset_sequence(); self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } } let movement_action_early = if let Some(act) = config.get_general_action(key_event.code, key_event.modifiers) { 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 outcome = add_logic::event::handle_add_logic_event( key_event, movement_action_early, config, app_state, add_logic_page, self.grpc_client.clone(), self.save_logic_result_sender.clone(), )?; if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } } else if let Page::AddTable(add_table_page) = &mut router.current { // Allow ":" (enter_command_mode) even when inside AddTable canvas if let Some(action) = config.get_general_action(key_event.code, key_event.modifiers) { if action == "enter_command_mode" && !self.command_mode && !app_state.ui.show_search_palette && !self.navigation_state.active { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); self.input_engine.reset_sequence(); self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } } // Handle AddTable before global actions so canvas gets first shot at keys. // Map keys to MovementAction (same as AddLogic early handler) let movement_action_early = if let Some(act) = config.get_general_action(key_event.code, key_event.modifiers) { 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 outcome = add_table::event::handle_add_table_event( key_event, movement_action_early, config, app_state, add_table_page, self.grpc_client.clone(), self.save_table_result_sender.clone(), )?; // Only stop if the page consumed the key; else let global handling proceed. 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, buffer_state, router, &mut self.command_message, )? { return Ok(EventOutcome::Ok(self.command_message.clone())); } } } } } // Sidebar/buffer toggles now handled via AppAction in the engine if current_mode == AppMode::General { // General mode specific key mapping now handled via AppAction } 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())); } } _ => {} } } // 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 => { // Command-mode keys already handled by the engine. // Collect characters not handled (typed command input). match key_code { KeyCode::Char(c) => { self.command_input.push(c); return Ok(EventOutcome::Ok(String::new())); } _ => { self.input_engine.reset_sequence(); 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" ) } fn set_focus_outside(&mut self, router: &mut Router, outside: bool) { match &mut router.current { Page::Login(state) => state.focus_outside_canvas = outside, Page::Register(state) => state.focus_outside_canvas = outside, Page::Intro(state) => state.focus_outside_canvas = outside, Page::Admin(state) => state.focus_outside_canvas = outside, Page::AddLogic(state) => state.focus_outside_canvas = outside, Page::AddTable(state) => state.focus_outside_canvas = outside, _ => {} } } fn set_focused_button(&mut self, router: &mut Router, index: usize) { match &mut router.current { Page::Login(state) => state.focused_button_index = index, Page::Register(state) => state.focused_button_index = index, Page::Intro(state) => state.focused_button_index = index, Page::Admin(state) => state.focused_button_index = index, Page::AddLogic(state) => state.focused_button_index = index, Page::AddTable(state) => state.focused_button_index = index, _ => {} } } fn is_focus_outside(&self, router: &Router) -> bool { match &router.current { Page::Login(state) => state.focus_outside_canvas, Page::Register(state) => state.focus_outside_canvas, Page::Intro(state) => state.focus_outside_canvas, Page::Admin(state) => state.focus_outside_canvas, Page::AddLogic(state) => state.focus_outside_canvas, Page::AddTable(state) => state.focus_outside_canvas, _ => false, } } fn focused_button(&self, router: &Router) -> usize { match &router.current { Page::Login(state) => state.focused_button_index, Page::Register(state) => state.focused_button_index, Page::Intro(state) => state.focused_button_index, Page::Admin(state) => state.focused_button_index, Page::AddLogic(state) => state.focused_button_index, Page::AddTable(state) => state.focused_button_index, _ => 0, } } async fn execute_command( &mut self, key_event: crossterm::event::KeyEvent, config: &Config, terminal: &mut TerminalCore, command_handler: &mut CommandHandler, app_state: &mut AppState, router: &mut Router, ) -> Result { 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.input_engine.reset_sequence(); let new_mode = ModeManager::derive_mode(app_state, self, router); app_state.update_mode(new_mode); Ok(outcome) } #[allow(clippy::too_many_arguments)] async fn handle_app_action( &mut self, action: AppAction, key_event: crossterm::event::KeyEvent, 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> { match action { AppAction::ToggleSidebar => { app_state.ui.show_sidebar = !app_state.ui.show_sidebar; let message = format!( "Sidebar {}", if app_state.ui.show_sidebar { "shown" } else { "hidden" } ); Ok(Some(EventOutcome::Ok(message))) } AppAction::ToggleBufferList => { app_state.ui.show_buffer_list = !app_state.ui.show_buffer_list; let message = format!( "Buffer {}", if app_state.ui.show_buffer_list { "shown" } else { "hidden" } ); Ok(Some(EventOutcome::Ok(message))) } AppAction::Buffer(BufferAction::Next) => { if switch_buffer(buffer_state, true) { return Ok(Some(EventOutcome::Ok( "Switched to next buffer".to_string(), ))); } Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::Buffer(BufferAction::Previous) => { if switch_buffer(buffer_state, false) { return Ok(Some(EventOutcome::Ok( "Switched to previous buffer".to_string(), ))); } Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::Buffer(BufferAction::Close) => { let current_table_name = app_state.current_view_table_name.as_deref(); let message = buffer_state .close_buffer_with_intro_fallback(current_table_name); Ok(Some(EventOutcome::Ok(message))) } AppAction::OpenSearch => { 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)); self.set_focus_outside(router, true); return Ok(Some(EventOutcome::Ok( "Search palette opened".to_string(), ))); } } Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::FindFilePaletteToggle => { 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.input_engine.reset_sequence(); return Ok(Some(EventOutcome::Ok( "Table selection palette activated".to_string(), ))); } Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::EnterCommandMode => { if !self.is_in_form_edit_mode(router, app_state) && !self.command_mode && !app_state.ui.show_search_palette && !self.navigation_state.active { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); self.input_engine.reset_sequence(); // Keep focus outside so canvas won’t consume keystrokes self.set_focus_outside(router, true); } Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::ExitCommandMode => { self.command_input.clear(); self.command_message.clear(); self.command_mode = false; self.input_engine.reset_sequence(); if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { editor.set_mode(CanvasMode::ReadOnly); } } Ok(Some(EventOutcome::Ok( "Exited command mode".to_string(), ))) } AppAction::CommandExecute => { // Execute using the actual configured key that triggered the action let out = self .execute_command( key_event, config, terminal, command_handler, app_state, router, ) .await?; Ok(Some(out)) } AppAction::CommandBackspace => { self.command_input.pop(); self.input_engine.reset_sequence(); Ok(Some(EventOutcome::Ok(String::new()))) } AppAction::Core(core) => { let s = match core { CoreAction::Save => "save", CoreAction::ForceQuit => "force_quit", CoreAction::SaveAndQuit => "save_and_quit", CoreAction::Revert => "revert", }; let out = self .handle_core_action(s, auth_state, terminal, app_state, router) .await?; Ok(Some(out)) } AppAction::Navigate(_ma) => { // Movement is still handled by page/nav code paths that // follow after PassThrough. We return None here to keep flow. Ok(None) } } } fn is_in_form_edit_mode(&self, router: &Router, app_state: &AppState) -> bool { if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path_ref(path) { return editor.mode() == CanvasMode::Edit; } } false } }