Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions rmk/src/config/positional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,14 @@ impl<const ROW: usize, const COL: usize> PositionalConfig<ROW, COL> {
Self { hand }
}
}

impl Hand {
/// Whether two keys sit on the same hand for unilateral-tap decisions.
///
/// Only `Left`/`Left` and `Right`/`Right` qualify. `Unknown` keys and
/// `Bilateral` keys (which are deliberately exempt from same-hand rules)
/// always return `false`, even when paired with themselves.
pub fn is_same_side(self, other: Hand) -> bool {
matches!((self, other), (Hand::Left, Hand::Left) | (Hand::Right, Hand::Right))
}
}
27 changes: 23 additions & 4 deletions rmk/src/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use rmk_types::mouse_button::MouseButtons;
use usbd_hid::descriptor::{MediaKeyboardReport, SystemControlReport};

use crate::channel::send_hid_report;
use crate::config::Hand;
use crate::core_traits::Runnable;
#[cfg(all(feature = "split", feature = "_ble"))]
use crate::event::ClearPeerEvent;
Expand Down Expand Up @@ -725,7 +724,28 @@ impl<'a> Keyboard<'a> {
let _ = decisions.push((held_key.event.pos, HeldKeyDecision::HoldOnOtherKeyPress));
decision_for_current_key = KeyBehaviorDecision::CleanBuffer;
}
_ => {}
MorseMode::Normal => {
// Normal mode: resolve a same-hand HRM as tap on press when
// unilateral_tap is enabled, so the roll fires in the correct
// order (HRM tap first, then the new key).
let unilateral_tap = Self::is_unilateral_tap_enabled(self.keymap, &held_key.action);
if unilateral_tap
&& matches!(held_key.state, KeyState::Pressed(_))
&& let KeyboardEventPos::Key(pos1) = held_key.event.pos
&& let KeyboardEventPos::Key(pos2) = event.pos
{
let hand1 = self.keymap.hand_at(pos1.row as usize, pos1.col as usize);
let hand2 = self.keymap.hand_at(pos2.row as usize, pos2.col as usize);
if hand1.is_same_side(hand2) {
debug!(
"Unilateral tap on press (Normal mode): resolving HRM as tap for correct roll order"
);
let _ = decisions.push((held_key.event.pos, HeldKeyDecision::UnilateralTap));
decision_for_current_key = KeyBehaviorDecision::CleanBuffer;
continue;
}
}
}
}
} else {
let unilateral_tap = Self::is_unilateral_tap_enabled(self.keymap, &held_key.action);
Expand All @@ -742,8 +762,7 @@ impl<'a> Keyboard<'a> {
let hand1 = self.keymap.hand_at(pos1.row as usize, pos1.col as usize);
let hand2 = self.keymap.hand_at(pos2.row as usize, pos2.col as usize);

if hand1 == hand2 && hand1 != Hand::Unknown && hand2 != Hand::Bilateral {
//if same hand
if hand1.is_same_side(hand2) {
debug!("Unilateral tap triggered, resolve morse key as tapping");
let _ = decisions.push((held_key.event.pos, HeldKeyDecision::UnilateralTap));
continue;
Expand Down
64 changes: 52 additions & 12 deletions rmk/tests/keyboard_morse_hrm_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/// Test cases for home row mod(HRM)
///
/// For HRM, `enable_flow_tap` and `unilateral_tap` is enabled, `prior-idle-time` will be considered.
///
/// Keyboard layout (1 row, 5 cols, 2 layers):
/// Col: 0 1 2 3 4
/// L0: [A, mt!(B, LShift), mt!(C, LGui), lt!(1, D), mt!(E, LAlt)]
/// L1: [Kp1, Kp2, Kp3, Kp4, Kp5]
///
/// Hand config: [Left, Left, Right, Right, Right]
pub mod common;

use embassy_time::Duration;
Expand All @@ -24,12 +31,7 @@ fn create_hrm_keyboard() -> Keyboard<'static> {
morse: MorsesConfig {
enable_flow_tap: true,
prior_idle_time: Duration::from_millis(120),
default_profile: MorseProfile::new(
Some(true),
Some(MorseMode::PermissiveHold),
Some(250u16),
Some(250u16),
),
default_profile: MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(250), Some(250)),
..Default::default()
},
..Default::default()
Expand Down Expand Up @@ -63,12 +65,7 @@ fn create_hrm_keyboard_with_combo() -> Keyboard<'static> {
morse: MorsesConfig {
enable_flow_tap: true,
prior_idle_time: Duration::from_millis(120),
default_profile: MorseProfile::new(
Some(true),
Some(MorseMode::PermissiveHold),
Some(250u16),
Some(250u16),
),
default_profile: MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(250), Some(250)),
..Default::default()
},
combo: CombosConfig {
Expand Down Expand Up @@ -1758,3 +1755,46 @@ fn test_release_morse_keeps_pressed_layer_transparent_action_after_layer_off_hol
]
};
}

fn create_normal_unilateral_keyboard() -> Keyboard<'static> {
let hand = [[Hand::Left, Hand::Left, Hand::Right, Hand::Right, Hand::Right]];
create_morse_keyboard(
BehaviorConfig {
morse: MorsesConfig {
enable_flow_tap: false,
default_profile: MorseProfile::new(
Some(true), // unilateral_tap enabled
Some(MorseMode::Normal), // Normal (timeout-only) hold
Some(250),
Some(250),
),
..Default::default()
},
..Default::default()
},
hand,
)
}

/// Same-hand roll in Normal mode: mt!(B, LShift) (col 1, Left) then A (col 0, Left).
/// The HRM tap must fire BEFORE the plain key so the roll comes out in the pressed order.
/// Previously, Normal mode + unilateral_tap only resolved on key-release, causing the
/// plain key to fire first (wrong order).
#[test]
fn test_normal_mode_same_hand_roll_order() {
key_sequence_test! {
keyboard: create_normal_unilateral_keyboard(),
sequence: [
[0, 1, true, 10], // Press mt!(B, LShift) — HRM, Left hand
[0, 0, true, 10], // Press A — plain key, Left hand (same-hand roll)
[0, 0, false, 10], // Release A
[0, 1, false, 10], // Release mt!(B, LShift)
],
expected_reports: [
[0, [kc_to_u8!(B), 0, 0, 0, 0, 0]], // B fires first (unilateral tap on press)
[0, [kc_to_u8!(B), kc_to_u8!(A), 0, 0, 0, 0]], // A fires after
[0, [kc_to_u8!(B), 0, 0, 0, 0, 0]], // A released
[0, [0, 0, 0, 0, 0, 0]], // B released
]
};
}