Skip to content

fix: tap-hold-opposite-hand-release skips same-hand keys when held#2017

Merged
jtroo merged 1 commit intojtroo:mainfrom
viktomas:fix/opposite-hand-release-same-hand-skip
Apr 11, 2026
Merged

fix: tap-hold-opposite-hand-release skips same-hand keys when held#2017
jtroo merged 1 commit intojtroo:mainfrom
viktomas:fix/opposite-hand-release-same-hand-skip

Conversation

@viktomas
Copy link
Copy Markdown
Contributor

@viktomas viktomas commented Apr 9, 2026

👋 hello, when I used the tap-hold-opposite-hand-release and pressed f+d for ctrl and shift modifiers and then pressed j, it outputted ctrl+d and ignored the f. Below is an AI-generated fix with tests. Feel free to close the issue since I didn't put too much effort into reviewing. But the fix works on my machine

Problem

tap-hold-opposite-hand-release with (same-hand tap) incorrectly resolves as Hold when two same-hand HRM keys are pressed together followed by an opposite-hand key.

Reproduction: Given left-hand HRM keys f→ctrl and d→shift, and right-hand key j:

d:f t:5 d:d t:20 d:j t:20 u:j

Expected: f resolves as Tap (same-hand d was pressed)
Actual: f resolves as Hold(ctrl), outputting ctrl+d

Root cause

The -release variant checks for both press+release in the queue before checking which hand a key belongs to:

// 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; // ← same-hand keys skipped when still held!
}
// hand check happens here, too late

When f is waiting and the queue contains [d↓, j↓, j↑]:

  1. d↓ — looks for d↑not found (d still held) → continue (skipped)
  2. j↓ — looks for j↑found → opposite hand → Hold

The same-hand key d is invisible because it hasn't been released yet.

Fix

Move the release check after the hand check. Only opposite-hand, neutral, and unknown-hand keys require press+release. Same-hand keys resolve immediately on press — matching tap-hold-opposite-hand (non-release variant) behavior for same-hand decisions.

This is correct because the -release requirement exists to prevent misfires on fast cross-hand overlaps (e.g., f↓ j↓ f↑ j↑ should be fj, not ctrl+j). Same-hand keys don't need this protection — they already resolve as Tap structurally.

Checklist

  • Add documentation to docs/config.adoc
    • N/A - it's a bug fix
  • Add example and basic docs to cfg_samples/kanata.kbd
  • N/A - it's a bug fix
  • Update error messages
  • N/A - it's a bug fix
  • Added tests, or did manual testing
    • Yes both automated and manual testing

The -release variant required every key to have both press+release in
the queue before considering it. This caused same-hand keys that were
still held (no release yet) to be skipped entirely. A subsequent
opposite-hand key with its release in the queue would then incorrectly
trigger Hold.

Example: f(ctrl) and d(shift) on left hand, j on right hand.
Pressing f↓ d↓ j↓ j↑ would resolve f as Hold(ctrl) because:
1. d↓ checked for d↑ → not found → skipped
2. j↓ checked for j↑ → found → opposite hand → Hold

Fix: apply the release requirement only to opposite-hand, neutral, and
unknown-hand keys. Same-hand keys resolve immediately on press, matching
the non-release variant (tap-hold-opposite-hand) behavior for same-hand
decisions.
@viktomas
Copy link
Copy Markdown
Contributor Author

viktomas commented Apr 9, 2026

This actually works quite well for me, if you would be tempted to close the issue, please let me know what's missing and I can try to add it 🙇

@jtroo jtroo merged commit 56775b1 into jtroo:main Apr 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants