From a1a3d29be61b52330dec7a6b55ad0ffcb7a7997b Mon Sep 17 00:00:00 2001 From: thairanaru Date: Fri, 17 Jul 2026 09:39:52 -0700 Subject: [PATCH 1/3] test: added tests for typing and single keybinding shortcuts --- src/action.rs | 2 +- src/input.rs | 145 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/action.rs b/src/action.rs index 6a5c104..44d4a54 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,5 +1,5 @@ /// Action based on user input -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { #[allow(unused)] AppendCharacter(char), diff --git a/src/input.rs b/src/input.rs index 6519d23..5fc5e59 100644 --- a/src/input.rs +++ b/src/input.rs @@ -24,6 +24,49 @@ struct KeyMaps { typing: HashMap, Action>, } +impl Default for KeyMaps { + fn default() -> Self { + Self { + ui: HashMap::from([( + vec![KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], + Action::Quit, + )]), + normal: HashMap::new(), + visual: HashMap::new(), + typing: HashMap::from([ + ( + vec![KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)], + Action::RemoveCharacter, + ), + ( + vec![KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)], + Action::Enter, + ), + ( + vec![KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)], + Action::CursorLeft, + ), + ( + vec![KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)], + Action::CursorRight, + ), + ( + vec![KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)], + Action::CursorUp, + ), + ( + vec![KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)], + Action::CursorDown, + ), + ( + vec![KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)], + Action::Escape, + ), + ]), + } + } +} + pub struct InputState { pub input_mode: InputMode, pending_keys: Vec, @@ -35,49 +78,17 @@ impl Default for InputState { Self { input_mode: InputMode::UI, pending_keys: Vec::with_capacity(2), - key_maps: KeyMaps { - ui: HashMap::from([( - vec![KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], - Action::Quit, - )]), - normal: HashMap::new(), - visual: HashMap::new(), - typing: HashMap::from([ - ( - vec![KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)], - Action::RemoveCharacter, - ), - ( - vec![KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)], - Action::Enter, - ), - ( - vec![KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)], - Action::CursorLeft, - ), - ( - vec![KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)], - Action::CursorRight, - ), - ( - vec![KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)], - Action::CursorUp, - ), - ( - vec![KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)], - Action::CursorDown, - ), - ( - vec![KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)], - Action::Escape, - ), - ]), - }, + key_maps: KeyMaps::default(), } } } impl InputState { + fn change_input_mode(&mut self, new_mode: InputMode) { + self.pending_keys.clear(); + self.input_mode = new_mode; + } + /// Gets respective keymap based on input mode fn key_map(&self) -> &HashMap, Action> { match self.input_mode { @@ -118,3 +129,61 @@ impl InputState { action } } + +#[cfg(test)] +mod test { + use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + + use crate::{ + action::Action, + input::{InputMode, InputState}, + }; + + #[test] + fn single_key_motions() { + let mut state = InputState::default(); + for _ in 0..2 { + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Null, KeyModifiers::NONE)), + None, + "Should have no action" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)), + Some(Action::Quit), + "Should have done the quit action" + ); + } + } + + #[test] + fn typing_motions() { + let mut state = InputState::default(); + state.change_input_mode(InputMode::Insert); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)), + Some(Action::AppendCharacter('q')), + "Should have done appened q" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)), + Some(Action::AppendCharacter('r')), + "Should have appened r" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::SHIFT)), + Some(Action::AppendCharacter('R')), + "Should have appened R" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Null, KeyModifiers::NONE)), + None, + "Should have no action" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)), + Some(Action::CursorLeft), + "Should have moved the cursor left" + ); + } +} From 85ea5c54fc31f4acccda245c67553058a343882a Mon Sep 17 00:00:00 2001 From: thairanaru Date: Fri, 17 Jul 2026 09:49:21 -0700 Subject: [PATCH 2/3] fix: update capital letters --- src/input.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/input.rs b/src/input.rs index 5fc5e59..d06e55b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -112,22 +112,28 @@ impl InputState { pub fn process_key_event(&mut self, key_event: KeyEvent) -> Option { self.pending_keys.push(key_event); let key_map = self.key_map(); - - let action = - key_map - .get(&self.pending_keys) - .cloned() - .or(match (self.input_mode, key_event.code) { - (InputMode::Insert | InputMode::Command, KeyCode::Char(c)) => { - Some(Action::AppendCharacter(c)) - } - _ => None, - }); + let action = key_map + .get(&self.pending_keys) + .cloned() + .or(self.handle_typing_event(key_event)); if action.is_some() || !self.has_potential_pending_key_bindings() { self.pending_keys.clear(); } action } + + pub fn handle_typing_event(&self, key_event: KeyEvent) -> Option { + match (self.input_mode, key_event.code) { + (InputMode::Insert | InputMode::Command, KeyCode::Char(c)) => { + match key_event.modifiers { + KeyModifiers::SHIFT => Some(Action::AppendCharacter(c.to_ascii_uppercase())), + KeyModifiers::NONE => Some(Action::AppendCharacter(c)), + _ => None, + } + } + _ => None, + } + } } #[cfg(test)] From 044806a25361c5c4d2ce553577d56d1d6502b7b5 Mon Sep 17 00:00:00 2001 From: thairanaru Date: Fri, 17 Jul 2026 11:21:38 -0700 Subject: [PATCH 3/3] feat: added GoToTop action with tests --- src/action.rs | 2 ++ src/input.rs | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/action.rs b/src/action.rs index 44d4a54..51c92cc 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,6 +1,8 @@ /// Action based on user input #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { + #[allow(unused)] + GoToTopUI, #[allow(unused)] AppendCharacter(char), RemoveCharacter, diff --git a/src/input.rs b/src/input.rs index d06e55b..2eb1d82 100644 --- a/src/input.rs +++ b/src/input.rs @@ -27,10 +27,19 @@ struct KeyMaps { impl Default for KeyMaps { fn default() -> Self { Self { - ui: HashMap::from([( - vec![KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], - Action::Quit, - )]), + ui: HashMap::from([ + ( + vec![KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], + Action::Quit, + ), + ( + vec![ + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE), + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE), + ], + Action::GoToTopUI, + ), + ]), normal: HashMap::new(), visual: HashMap::new(), typing: HashMap::from([ @@ -84,6 +93,7 @@ impl Default for InputState { } impl InputState { + #[allow(unused)] fn change_input_mode(&mut self, new_mode: InputMode) { self.pending_keys.clear(); self.input_mode = new_mode; @@ -192,4 +202,24 @@ mod test { "Should have moved the cursor left" ); } + + #[test] + fn multi_keybinding_shortcuts() { + let mut state = InputState::default(); + assert!( + state.process_key_event(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE)) + != Some(Action::GoToTopUI), + "Should not called action early" + ); + assert_eq!( + state.process_key_event(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE)), + Some(Action::GoToTopUI), + "Should have go to top UI after second g" + ); + assert!( + state.process_key_event(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE)) + != Some(Action::GoToTopUI), + "Should have cleared pending keys" + ); + } }