diff --git a/client/config.toml b/client/config.toml index 764dd4b..7ef46d2 100644 --- a/client/config.toml +++ b/client/config.toml @@ -2,10 +2,14 @@ [keybindings] enter_command_mode = [":", "ctrl+;"] -next_buffer = ["space+b+n"] -previous_buffer = ["space+b+p"] -close_buffer = ["space+b+d"] -revert = ["space+b+r"] +next_buffer = ["ctrl+b+n"] +previous_buffer = ["ctrl+b+p"] +close_buffer = ["ctrl+b+d"] +# SPACE NOT WORKING, NEEDS REDESIGN +# next_buffer = ["space+b+n"] +# previous_buffer = ["space+b+p"] +# close_buffer = ["space+b+d"] +# revert = ["space+b+r"] [keybindings.general] up = ["k", "Up"] diff --git a/client/src/input/action.rs b/client/src/input/action.rs new file mode 100644 index 0000000..4d8ec80 --- /dev/null +++ b/client/src/input/action.rs @@ -0,0 +1,41 @@ +// src/input/action.rs +use crate::movement::MovementAction; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BufferAction { + Next, + Previous, + Close, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CoreAction { + Save, + ForceQuit, + SaveAndQuit, + Revert, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AppAction { + // Global/UI + ToggleSidebar, + ToggleBufferList, + OpenSearch, + FindFilePaletteToggle, + + // Buffers + Buffer(BufferAction), + + // Command mode + EnterCommandMode, + ExitCommandMode, + CommandExecute, + CommandBackspace, + + // Navigation across UI + Navigate(MovementAction), + + // Core actions + Core(CoreAction), +} diff --git a/client/src/input/engine.rs b/client/src/input/engine.rs new file mode 100644 index 0000000..6570090 --- /dev/null +++ b/client/src/input/engine.rs @@ -0,0 +1,176 @@ +// src/input/engine.rs +use crate::config::binds::config::Config; +use crate::config::binds::key_sequences::KeySequenceTracker; +use crate::input::action::{AppAction, BufferAction, CoreAction}; +use crate::movement::MovementAction; +use crate::modes::handlers::mode_manager::AppMode; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +#[derive(Debug, Clone, Copy)] +pub struct InputContext { + pub app_mode: AppMode, + pub overlay_active: bool, + pub allow_navigation_capture: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputOutcome { + Action(AppAction), + Pending, // sequence in progress + PassThrough, // let page/canvas handle it +} + +pub struct InputEngine { + seq: KeySequenceTracker, +} + +impl InputEngine { + pub fn new(timeout_ms: u64) -> Self { + Self { + seq: KeySequenceTracker::new(timeout_ms), + } + } + + pub fn reset_sequence(&mut self) { + self.seq.reset(); + } + + pub fn process_key( + &mut self, + key_event: KeyEvent, + ctx: &InputContext, + config: &Config, + ) -> InputOutcome { + // Command mode keys are special (exit/execute/backspace) and typed chars + if ctx.app_mode == AppMode::Command { + if config.is_exit_command_mode(key_event.code, key_event.modifiers) { + self.seq.reset(); + return InputOutcome::Action(AppAction::ExitCommandMode); + } + if config.is_command_execute(key_event.code, key_event.modifiers) { + self.seq.reset(); + return InputOutcome::Action(AppAction::CommandExecute); + } + if config.is_command_backspace(key_event.code, key_event.modifiers) { + self.seq.reset(); + return InputOutcome::Action(AppAction::CommandBackspace); + } + // Let command-line collect characters and other keys pass through + self.seq.reset(); + return InputOutcome::PassThrough; + } + + // If overlays are active, do not intercept (palette, navigation, etc.) + if ctx.overlay_active { + self.seq.reset(); + return InputOutcome::PassThrough; + } + + // Space-led multi-key sequences (leader = space) + if ctx.allow_navigation_capture { + let space = KeyCode::Char(' '); + let seq_active = !self.seq.current_sequence.is_empty() + && self.seq.current_sequence[0] == space; + + if seq_active { + self.seq.add_key(key_event.code); + let sequence = self.seq.get_sequence(); + + if let Some(action_str) = config.matches_key_sequence_generalized(&sequence) { + if let Some(app_action) = map_action_string(action_str, ctx) { + self.seq.reset(); + return InputOutcome::Action(app_action); + } + // A non-app action sequence (canvas stuff) → pass-through + self.seq.reset(); + return InputOutcome::PassThrough; + } + + if config.is_key_sequence_prefix(&sequence) { + return InputOutcome::Pending; + } + + // Not matched and not a prefix → reset and continue to single key + self.seq.reset(); + } else if key_event.code == space && config.is_key_sequence_prefix(&[space]) { + self.seq.reset(); + self.seq.add_key(space); + return InputOutcome::Pending; + } + } + + // Single-key mapping: try general binds first (arrows, open_search, enter_command_mode) + if let Some(action_str) = + config.get_general_action(key_event.code, key_event.modifiers) + { + if let Some(app_action) = map_action_string(action_str, ctx) { + return InputOutcome::Action(app_action); + } + // Unknown to app layer (likely canvas movement etc.) → pass + return InputOutcome::PassThrough; + } + + // Then app-level common/read-only/edit/highlight for UI toggles or core actions + if let Some(action_str) = config.get_app_action(key_event.code, key_event.modifiers) { + if let Some(app_action) = map_action_string(action_str, ctx) { + return InputOutcome::Action(app_action); + } + } + + InputOutcome::PassThrough + } + + /// Check if a key sequence is currently active + pub fn has_active_sequence(&self) -> bool { + !self.seq.current_sequence.is_empty() + } +} + +fn str_to_movement(s: &str) -> Option { + match s { + "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, + } +} + +fn map_action_string(action: &str, ctx: &InputContext) -> Option { + match action { + // Global/UI + "toggle_sidebar" => Some(AppAction::ToggleSidebar), + "toggle_buffer_list" => Some(AppAction::ToggleBufferList), + "open_search" => Some(AppAction::OpenSearch), + "find_file_palette_toggle" => Some(AppAction::FindFilePaletteToggle), + + // Buffers + "next_buffer" => Some(AppAction::Buffer(BufferAction::Next)), + "previous_buffer" => Some(AppAction::Buffer(BufferAction::Previous)), + "close_buffer" => Some(AppAction::Buffer(BufferAction::Close)), + + // Command mode + "enter_command_mode" => Some(AppAction::EnterCommandMode), + "exit_command_mode" => Some(AppAction::ExitCommandMode), + "command_execute" => Some(AppAction::CommandExecute), + "command_backspace" => Some(AppAction::CommandBackspace), + + // Navigation across UI (only if allowed) + s if str_to_movement(s).is_some() && ctx.allow_navigation_capture => { + Some(AppAction::Navigate(str_to_movement(s).unwrap())) + } + + // Core actions + "save" => Some(AppAction::Core(CoreAction::Save)), + "force_quit" => Some(AppAction::Core(CoreAction::ForceQuit)), + "save_and_quit" => Some(AppAction::Core(CoreAction::SaveAndQuit)), + "revert" => Some(AppAction::Core(CoreAction::Revert)), + + // Unknown to app layer: ignore (canvas-specific actions, etc.) + _ => None, + } +} diff --git a/client/src/input/mod.rs b/client/src/input/mod.rs new file mode 100644 index 0000000..79c795c --- /dev/null +++ b/client/src/input/mod.rs @@ -0,0 +1,3 @@ +// src/input/mod.rs +pub mod action; +pub mod engine; diff --git a/client/src/lib.rs b/client/src/lib.rs index cee60d8..cf1da55 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -14,6 +14,7 @@ pub mod search; pub mod bottom_panel; pub mod pages; pub mod movement; +pub mod input; pub use ui::run_ui; diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 748f88e..5551b3b 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -1,6 +1,7 @@ // src/modes/handlers/event.rs use crate::config::binds::config::Config; -use crate::config::binds::key_sequences::KeySequenceTracker; +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; @@ -76,7 +77,7 @@ pub struct EventHandler { pub command_message: String, pub edit_mode_cooldown: bool, pub ideal_cursor_column: usize, - pub key_sequence_tracker: KeySequenceTracker, + pub input_engine: InputEngine, pub auth_client: AuthClient, pub grpc_client: GrpcClient, pub login_result_sender: mpsc::Sender, @@ -106,7 +107,7 @@ impl EventHandler { command_message: String::new(), edit_mode_cooldown: false, ideal_cursor_column: 0, - key_sequence_tracker: KeySequenceTracker::new(1200), + input_engine: InputEngine::new(1200), auth_client: AuthClient::new().await?, grpc_client, login_result_sender, @@ -301,67 +302,54 @@ impl EventHandler { || app_state.ui.show_search_palette || self.navigation_state.active; - // --- SPACE-LED SEQUENCES INTERCEPTION (all modes except Form Edit) --- - if !overlay_active { - // Detect if we are in a Form and the canvas is in Edit mode (then we don't intercept) - 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 } - } - ); - - if !in_form_edit_mode { - let space = KeyCode::Char(' '); - let seq_active = !self.key_sequence_tracker.current_sequence.is_empty() - && self.key_sequence_tracker.current_sequence[0] == space; - - if seq_active { - self.key_sequence_tracker.add_key(key_code); - let sequence = self.key_sequence_tracker.get_sequence(); - - if let Some(action) = config.matches_key_sequence_generalized(&sequence) { - self.key_sequence_tracker.reset(); - if let Some(outcome) = self - .dispatch_space_sequence_action( - action, - config, - terminal, - command_handler, - auth_state, - buffer_state, - app_state, - router, - ) - .await? - { - return Ok(outcome); - } else { - return Ok(EventOutcome::Ok(String::new())); - } - } - - if config.is_key_sequence_prefix(&sequence) { - return Ok(EventOutcome::Ok(String::new())); - } - - // Not matched and not prefix → reset and fall through - self.key_sequence_tracker.reset(); - } else if key_code == space && config.is_key_sequence_prefix(&[space]) { - // Start new space-led sequence and do not let the page consume the space - self.key_sequence_tracker.reset(); - self.key_sequence_tracker.add_key(space); - return Ok(EventOutcome::Ok(String::new())); + // 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 input_ctx = InputContext { + app_mode: current_mode, + overlay_active, + allow_navigation_capture: !in_form_edit_mode && !overlay_active, + }; + match self.input_engine.process_key(key_event, &input_ctx, config) { + 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 } } - // --- END SPACE-LED SEQUENCES --- // 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 = @@ -397,7 +385,7 @@ impl EventHandler { } } else if let Page::Form(path) = &router.current { // If a space-led sequence is in progress or has just begun, do not forward to editor - if self.key_sequence_tracker.current_sequence.is_empty() { + if !self.input_engine.has_active_sequence() { let outcome = forms::event::handle_form_event( event, app_state, @@ -424,7 +412,7 @@ impl EventHandler { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); - self.key_sequence_tracker.reset(); + self.input_engine.reset_sequence(); self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } @@ -470,7 +458,7 @@ impl EventHandler { self.command_mode = true; self.command_input.clear(); self.command_message.clear(); - self.key_sequence_tracker.reset(); + self.input_engine.reset_sequence(); self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } @@ -526,116 +514,11 @@ impl EventHandler { } } } - 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)); - } + + // Sidebar/buffer toggles now handled via AppAction in the engine 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)); - } - "revert" | "save" | "force_quit" | "save_and_quit" => { - let outcome = self.handle_core_action( - action, - auth_state, - terminal, - app_state, - router, - ).await?; - return Ok(outcome); - } - _ => {} - } - } - - 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)); - self.set_focus_outside(router, true); - return Ok(EventOutcome::Ok( - "Search palette opened".to_string(), - )); - } - } - } - // Allow ":" / ctrl+; to enter command mode only when outside canvas. - if action == "enter_command_mode" { - if self.is_focus_outside(router) - && !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.key_sequence_tracker.reset(); - // Keep focus outside so canvas won't receive keys - self.set_focus_outside(router, true); - return Ok(EventOutcome::Ok(String::new())); - } - } - } + // General mode specific key mapping now handled via AppAction } match current_mode { @@ -767,79 +650,18 @@ impl EventHandler { } 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); - } + // 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())); } - 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.input_engine.reset_sequence(); + return Ok(EventOutcome::Ok(String::new())); } - 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())); - } - - // Default command-mode handling: we do not treat Space as a leader here. - // Space-led sequences are handled only in General mode above. - if let KeyCode::Char(c) = key_code { - 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 { @@ -1027,13 +849,59 @@ impl EventHandler { _ => 0, } } -} -impl EventHandler { - #[allow(clippy::too_many_arguments)] - async fn dispatch_space_sequence_action( + async fn execute_command( &mut self, - action: &str, + 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, @@ -1043,58 +911,66 @@ impl EventHandler { router: &mut Router, ) -> Result> { match action { - // Reuse existing behavior - "next_buffer" => { - if switch_buffer(buffer_state, true) { - return Ok(Some(EventOutcome::Ok( - "Switched to next buffer".to_string(), - ))); - } - Ok(Some(EventOutcome::Ok(String::new()))) - } - "previous_buffer" => { - if switch_buffer(buffer_state, false) { - return Ok(Some(EventOutcome::Ok( - "Switched to previous buffer".to_string(), - ))); - } - Ok(Some(EventOutcome::Ok(String::new()))) - } - "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); + 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))) } - "open_search" => { + 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(), + "Search palette opened".to_string(), ))); } } Ok(Some(EventOutcome::Ok(String::new()))) } - "enter_command_mode" => { - if self.is_focus_outside(router) - && !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.key_sequence_tracker.reset(); - self.set_focus_outside(router, true); - return Ok(Some(EventOutcome::Ok(String::new()))); - } - Ok(Some(EventOutcome::Ok(String::new()))) - } - "find_file_palette_toggle" => { + AppAction::FindFilePaletteToggle => { if matches!(&router.current, Page::Form(_) | Page::Intro(_)) { let mut all_table_paths: Vec = app_state .profile_tree @@ -1105,34 +981,95 @@ impl EventHandler { format!("{}/{}", profile.name, table.name) }) }) - .collect(); + .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(); + self.input_engine.reset_sequence(); return Ok(Some(EventOutcome::Ok( - "Table selection palette activated".to_string(), + "Table selection palette activated".to_string(), ))); } Ok(Some(EventOutcome::Ok(String::new()))) } - // Core actions that already have a handler - "save" | "force_quit" | "save_and_quit" | "revert" => { - let outcome = self - .handle_core_action( - action, - auth_state, + 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(outcome)) + 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) } - _ => 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 + } }