space commands here we go again

This commit is contained in:
Priec
2025-09-11 22:36:40 +02:00
parent a604d62d44
commit 0d80266e9b
11 changed files with 331 additions and 141 deletions

View File

@@ -5,6 +5,8 @@ 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 {
@@ -22,107 +24,124 @@ pub enum InputOutcome {
pub struct InputEngine {
seq: KeySequenceTracker,
leader_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;
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),
}
}
if config.is_key_sequence_prefix(&sequence) {
return InputOutcome::Pending;
}
// Not matched and not a prefix → reset and continue to single key
pub fn reset_sequence(&mut self) {
info!("InputEngine.reset_sequence() leader_seq_before={:?}", self.leader_seq.current_sequence);
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;
}
self.leader_seq.reset();
}
// 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()
|| !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
}
}

View File

@@ -0,0 +1,74 @@
// src/input/leader.rs
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::parse_binding;
use crossterm::event::KeyCode;
/// Collect leader (= space-prefixed) bindings from *all* binding maps
fn leader_bindings<'a>(config: &'a Config) -> Vec<(&'a str, Vec<KeyCode>)> {
let mut out = Vec::new();
// Include all keybinding maps, not just global
let all_modes: Vec<&std::collections::HashMap<String, Vec<String>>> = vec![
&config.keybindings.general,
&config.keybindings.read_only,
&config.keybindings.edit,
&config.keybindings.highlight,
&config.keybindings.command,
&config.keybindings.common,
&config.keybindings.global,
];
for mode in all_modes {
for (action, bindings) in mode {
for b in bindings {
let parsed = parse_binding(b);
if parsed.first().map(|pk| pk.code) == Some(KeyCode::Char(' ')) {
let codes =
parsed.into_iter().map(|pk| pk.code).collect::<Vec<_>>();
out.push((action.as_str(), codes));
}
}
}
}
out
}
/// Is there any leader binding configured at all?
pub fn leader_has_any_start(config: &Config) -> bool {
leader_bindings(config)
.iter()
.any(|(_, seq)| seq.first() == Some(&KeyCode::Char(' ')))
}
/// Is `sequence` a prefix of any configured leader sequence?
pub fn leader_is_prefix(config: &Config, sequence: &[KeyCode]) -> bool {
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
return false;
}
for (_, full) in leader_bindings(config) {
if full.len() > sequence.len()
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
{
return true;
}
}
false
}
/// Is `sequence` an exact leader match? If yes, return the action string.
pub fn leader_match_action<'a>(
config: &'a Config,
sequence: &[KeyCode],
) -> Option<&'a str> {
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
return None;
}
for (action, full) in leader_bindings(config) {
if full.len() == sequence.len()
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
{
return Some(action);
}
}
None
}

View File

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