// 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}; use crate::input::leader::{leader_has_any_start, leader_is_prefix, leader_match_action}; use tracing::info; #[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, leader_seq: KeySequenceTracker, } impl InputEngine { pub fn new(normal_timeout_ms: u64, leader_timeout_ms: u64) -> Self { Self { seq: KeySequenceTracker::new(normal_timeout_ms), leader_seq: KeySequenceTracker::new(leader_timeout_ms), } } pub fn reset_sequence(&mut self) { info!("InputEngine.reset_sequence() leader_seq_before={:?}", self.leader_seq.current_sequence); self.seq.reset(); self.leader_seq.reset(); } pub fn has_active_sequence(&self) -> bool { !self.seq.current_sequence.is_empty() || !self.leader_seq.current_sequence.is_empty() } 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(); // Also reset leader sequence to avoid leaving a stale "space" active info!("Overlay active → reset leader_seq (was {:?})", self.leader_seq.current_sequence); self.leader_seq.reset(); return InputOutcome::PassThrough; } // Space-led multi-key sequences (leader = space) let space = KeyCode::Char(' '); let leader_active = !self.leader_seq.current_sequence.is_empty() && self.leader_seq.current_sequence[0] == space; // Keep collecting leader sequence even if allow_navigation_capture is false. if leader_active { self.leader_seq.add_key(key_event.code); let sequence = self.leader_seq.get_sequence(); info!( "Leader active updated: {:?} (added {:?})", sequence, key_event.code ); if let Some(action_str) = leader_match_action(config, &sequence) { info!("Leader matched '{}' with sequence {:?}", action_str, sequence); if let Some(app_action) = map_action_string(action_str, ctx) { self.leader_seq.reset(); return InputOutcome::Action(app_action); } self.leader_seq.reset(); return InputOutcome::PassThrough; } if leader_is_prefix(config, &sequence) { info!("Leader prefix continuing..."); return InputOutcome::Pending; } info!("Leader sequence reset (no match/prefix)."); self.leader_seq.reset(); // fall through to regular handling of this key } else if ctx.allow_navigation_capture && key_event.code == space && leader_has_any_start(config) { // Start a leader sequence only if capturing is allowed self.leader_seq.reset(); self.leader_seq.add_key(space); info!("Leader started: {:?}", self.leader_seq.get_sequence()); 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 } } 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, } }