inputs from keyboard are now decoupled
This commit is contained in:
41
client/src/input/action.rs
Normal file
41
client/src/input/action.rs
Normal file
@@ -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),
|
||||
}
|
||||
176
client/src/input/engine.rs
Normal file
176
client/src/input/engine.rs
Normal file
@@ -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<MovementAction> {
|
||||
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<AppAction> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
3
client/src/input/mod.rs
Normal file
3
client/src/input/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// src/input/mod.rs
|
||||
pub mod action;
|
||||
pub mod engine;
|
||||
Reference in New Issue
Block a user