diff --git a/rmk/src/keyboard.rs b/rmk/src/keyboard.rs index 35af33da..442b4af8 100644 --- a/rmk/src/keyboard.rs +++ b/rmk/src/keyboard.rs @@ -3,7 +3,7 @@ use crate::{ hid::{ConnectionType, HidWriterWrapper}, keyboard_macro::{MacroOperation, NUM_MACRO}, keycode::{KeyCode, ModifierCombination}, - keymap::KeyMap, + keymap::{Combo, KeyMap, COMBO_MAX_LENGTH}, usb::descriptor::{CompositeReport, CompositeReportType, ViaReport}, KEYBOARD_STATE, }; @@ -15,7 +15,7 @@ use embassy_sync::{ channel::{Channel, Receiver, Sender}, }; use embassy_time::{Instant, Timer}; -use heapless::{FnvIndexMap, Vec}; +use heapless::{Deque, FnvIndexMap, Vec}; use postcard::experimental::max_size::MaxSize; use rmk_config::BehaviorConfig; use serde::{Deserialize, Serialize}; @@ -159,6 +159,9 @@ pub(crate) struct Keyboard<'a, const ROW: usize, const COL: usize, const NUM_LAY /// The current distance of mouse key moving mouse_key_move_delta: i8, mouse_wheel_move_delta: i8, + + /// Buffer for pressed `KeyAction` and `KeyEvents` in combos + combo_actions_buffer: Deque<(KeyAction, KeyEvent), COMBO_MAX_LENGTH>, } impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> @@ -191,6 +194,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> last_mouse_tick: FnvIndexMap::new(), mouse_key_move_delta: 8, mouse_wheel_move_delta: 1, + combo_actions_buffer: Deque::new(), } } @@ -270,11 +274,18 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> } // Process key - let action = self + let key_action = self .keymap .borrow_mut() .get_action_with_layer_cache(key_event); - match action { + + if let Some(key_action) = self.process_combo(key_action, key_event).await { + self.process_key_action(key_action, key_event).await; + } + } + + async fn process_key_action(&mut self, key_action: KeyAction, key_event: KeyEvent) { + match key_action { KeyAction::No | KeyAction::Transparent => (), KeyAction::Single(a) => self.process_key_action_normal(a, key_event).await, KeyAction::WithModifier(a, m) => { @@ -307,6 +318,69 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> } } + async fn process_combo( + &mut self, + key_action: KeyAction, + key_event: KeyEvent, + ) -> Option { + for combo in self.keymap.borrow_mut().combos.iter_mut() { + if !key_event.pressed && combo.done() && combo.actions.contains(&key_action) { + combo.reset(); + return Some(combo.output); + } + } + + if self + .combo_actions_buffer + .push_back((key_action, key_event)) + .is_err() + { + error!("Combo actions buffer overflowed! This is a bug and should not happen!"); + } + + let mut is_combo_action = false; + for combo in self.keymap.borrow_mut().combos.iter_mut() { + is_combo_action |= combo.update(key_action); + } + + if is_combo_action && key_event.pressed { + let next_action = self + .keymap + .borrow_mut() + .combos + .iter() + .find_map(|combo| combo.done().then_some(combo.output)); + + if next_action.is_some() { + self.combo_actions_buffer.clear(); + } else { + let timeout = embassy_time::Timer::after_millis(50); + match select(timeout, key_event_channel.receive()).await { + embassy_futures::select::Either::First(_) => self.dispatch_combos().await, + embassy_futures::select::Either::Second(event) => { + self.unprocessed_events.push(event).unwrap() + } + } + } + next_action + } else { + self.dispatch_combos().await; + None + } + } + + async fn dispatch_combos(&mut self) { + while let Some((action, event)) = self.combo_actions_buffer.pop_front() { + self.process_key_action(action, event).await; + } + self.keymap + .borrow_mut() + .combos + .iter_mut() + .filter(|c| !c.done()) + .for_each(Combo::reset); + } + async fn update_osm(&mut self, key_event: KeyEvent) { match self.osm_state { OneShotState::Initial(m) => self.osm_state = OneShotState::Held(m), diff --git a/rmk/src/keymap.rs b/rmk/src/keymap.rs index b4674eb8..8d5bb425 100644 --- a/rmk/src/keymap.rs +++ b/rmk/src/keymap.rs @@ -8,8 +8,59 @@ use crate::{ }; use defmt::{error, warn}; use embedded_storage_async::nor_flash::NorFlash; +use heapless::Vec; use num_enum::FromPrimitive; +pub(crate) const COMBO_MAX_NUM: usize = 8; +pub(crate) const COMBO_MAX_LENGTH: usize = 4; + +pub(crate) struct Combo { + pub(crate) actions: Vec, + pub(crate) output: KeyAction, + state: u8, +} + +impl Combo { + pub fn new(actions: Vec, output: KeyAction) -> Self { + Self { + actions, + output, + state: 0, + } + } + + pub fn empty() -> Self { + Self::new(Vec::new(), KeyAction::No) + } + + pub fn update(&mut self, key_action: KeyAction) -> bool { + let action_idx = self.actions.iter().position(|&a| a == key_action); + if let Some(i) = action_idx { + self.state |= 1 << i; + true + } else { + self.reset(); + false + } + } + + pub fn done(&self) -> bool { + self.started() && self.keys_pressed() == self.actions.len() as u32 + } + + pub fn started(&self) -> bool { + self.state != 0 + } + + pub fn keys_pressed(&self) -> u32 { + self.state.count_ones() + } + + pub fn reset(&mut self) { + self.state = 0; + } +} + /// Keymap represents the stack of layers. /// /// The conception of Keymap in rmk is borrowed from qmk: . @@ -27,6 +78,8 @@ pub(crate) struct KeyMap<'a, const ROW: usize, const COL: usize, const NUM_LAYER layer_cache: [[u8; COL]; ROW], /// Macro cache pub(crate) macro_cache: [u8; MACRO_SPACE_SIZE], + /// Combos + pub(crate) combos: [Combo; COMBO_MAX_NUM], } impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> @@ -39,6 +92,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> default_layer: 0, layer_cache: [[0; COL]; ROW], macro_cache: [0; MACRO_SPACE_SIZE], + combos: [(); COMBO_MAX_NUM].map(|_| Combo::empty()), } } @@ -80,6 +134,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize> default_layer: 0, layer_cache: [[0; COL]; ROW], macro_cache, + combos: [(); COMBO_MAX_NUM].map(|_| Combo::empty()), } }