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
42 changes: 30 additions & 12 deletions parser/src/cfg/custom_tap_hold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,14 @@ pub(crate) fn custom_tap_hold_opposite_hand(
}

/// Like `custom_tap_hold_opposite_hand` but waits for the interrupting key's
/// press+release before committing. This avoids misfires on fast same-hand
/// rolls where keystrokes briefly overlap.
/// press+release before committing to Hold. This avoids misfires on fast
/// cross-hand overlaps where keystrokes briefly overlap.
///
/// The `-release` requirement only applies to opposite-hand and unknown-hand
/// keys. Same-hand keys resolve immediately on press (no release needed),
/// because requiring release would cause same-hand keys to be skipped when
/// still held, allowing a later opposite-hand key+release to incorrectly
/// trigger Hold.
pub(crate) fn custom_tap_hold_opposite_hand_release(
hand_map: &'static HandMap,
same_hand: DecisionBehavior,
Expand All @@ -318,15 +324,14 @@ pub(crate) fn custom_tap_hold_opposite_hand_release(
continue;
}

// Wait for the interrupting key's release before deciding.
let release = Event::Release(i, j);
if !queued.clone().copied().any(|q| q.event() == release) {
continue;
}

// Check neutral-keys first (takes precedence over defhands)
if let Some(osc) = OsCode::from_u16(j) {
if neutral_keys.contains(&osc) {
// Neutral keys require release before deciding
let release = Event::Release(i, j);
if !queued.clone().copied().any(|q| q.event() == release) {
continue;
}
match neutral_behavior {
DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false),
DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false),
Expand All @@ -338,16 +343,29 @@ pub(crate) fn custom_tap_hold_opposite_hand_release(
let pressed_hand = hand_map.get(j);

match (waiting_hand, pressed_hand) {
(Hand::Left, Hand::Right) | (Hand::Right, Hand::Left) => {
return (Some(WaitingAction::Hold), false);
}
// Same hand: resolve immediately on press (no release needed).
// This prevents same-hand keys from being skipped while held,
// which would let a later opposite-hand release trigger Hold.
(Hand::Left, Hand::Left) | (Hand::Right, Hand::Right) => match same_hand {
DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false),
DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false),
DecisionBehavior::Ignore => continue,
},
// Opposite hand: require release before committing to Hold
(Hand::Left, Hand::Right) | (Hand::Right, Hand::Left) => {
let release = Event::Release(i, j);
if !queued.clone().copied().any(|q| q.event() == release) {
continue;
}
return (Some(WaitingAction::Hold), false);
}
_ => {
// At least one key is Neutral (not in defhands)
// At least one key is Neutral (not in defhands):
// require release before deciding
let release = Event::Release(i, j);
if !queued.clone().copied().any(|q| q.event() == release) {
continue;
}
match unknown_hand {
DecisionBehavior::Tap => return (Some(WaitingAction::Tap), false),
DecisionBehavior::Hold => return (Some(WaitingAction::Hold), false),
Expand Down
40 changes: 33 additions & 7 deletions src/tests/sim_tests/tap_hold_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,14 +301,14 @@ fn tap_hold_opposite_hand_release_same_hand_tap() {
s j
)
";
// Same-hand key pressed + released β†’ tap
// Same-hand key pressed + released β†’ tap (resolves on press, not release)
let result = simulate(cfg, "d:a t:20 d:s t:20 u:s t:100").to_ascii();
assert_eq!("t:40ms dn:X t:6ms dn:S t:1ms up:S", result);
assert_eq!("t:20ms dn:X t:6ms dn:S t:14ms up:S", result);

// Same-hand key pressed but NOT released β†’ no decision (waits for release)
// Eventually times out β†’ timeout action (tap by default)
// Same-hand key pressed but NOT released β†’ tap immediately on press
// (same-hand doesn't require release, only opposite-hand does)
let result = simulate(cfg, "d:a t:20 d:s t:250").to_ascii();
assert_eq!("t:200ms dn:X t:1ms dn:S", result);
assert_eq!("t:20ms dn:X t:6ms dn:S", result);
}

#[test]
Expand Down Expand Up @@ -400,7 +400,7 @@ fn tap_hold_opposite_hand_release_with_require_prior_idle() {

#[test]
fn tap_hold_opposite_hand_release_same_hand_hold() {
// (same-hand hold) should trigger hold when same-hand key is pressed+released
// (same-hand hold) should trigger hold immediately on press (no release needed)
let cfg = "
(defhands
(left a s d f)
Expand All @@ -412,7 +412,33 @@ fn tap_hold_opposite_hand_release_same_hand_hold() {
)
";
let result = simulate(cfg, "d:a t:20 d:s t:20 u:s t:100").to_ascii();
assert_eq!("t:40ms dn:Y t:6ms dn:S t:1ms up:S", result);
assert_eq!("t:20ms dn:Y t:6ms dn:S t:14ms up:S", result);
}

#[test]
fn tap_hold_opposite_hand_release_same_hand_then_opposite() {
// Regression test: pressing two same-hand HRM keys together then an
// opposite-hand key should resolve the first as tap (via same-hand),
// not as hold (via opposite-hand release).
// Bug: the -release variant required ALL keys to have press+release before
// considering them, so same-hand keys were skipped if still held, and the
// opposite-hand key's release incorrectly triggered Hold.
let cfg = "
(defcfg concurrent-tap-hold yes process-unmapped-keys yes)
(defhands
(left a s d f)
(right j k l ;))
(defsrc d f j)
(deflayer l1
(tap-hold-opposite-hand-release 500 d lsft (same-hand tap) (timeout tap))
(tap-hold-opposite-hand-release 500 f lctl (same-hand tap) (timeout tap))
j
)
";
// f↓ d↓ j↓ j↑ β†’ f sees d (same-hand) β†’ tap immediately; d sees j (opposite+release) β†’ hold
// Result: f (tap) at t=5, then lsft+j when j is released at t=45
let result = simulate(cfg, "d:f t:5 d:d t:20 d:j t:20 u:j t:100").to_ascii();
assert_eq!("t:5ms dn:F t:40ms dn:LShift t:6ms dn:J t:1ms up:J", result);
}

#[test]
Expand Down
Loading