inputs from keyboard are now decoupled

This commit is contained in:
Priec
2025-09-10 22:12:22 +02:00
parent 2cbbfd21aa
commit a604d62d44
6 changed files with 460 additions and 298 deletions

View 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
View 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
View File

@@ -0,0 +1,3 @@
// src/input/mod.rs
pub mod action;
pub mod engine;