-
-
Notifications
You must be signed in to change notification settings - Fork 39.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Core] Add Chordal Hold, an "opposite hands rule" tap-hold option similar to Achordion, Bilateral Combinations. #24560
base: develop
Are you sure you want to change the base?
Conversation
docs/tap_hold.md
Outdated
|
||
* Chordal Hold has no effect after the tapping term. | ||
|
||
* Chordal Hold has no effect when the other key is also a tap-hold key. This is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chordal Hold has no effect when the other key is also a tap-hold key.
Is this a limitation of the implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! Great comment. I was originally thinking multiple simultaneous tap-hold keys is out of scope of what Chordal Hold needs to consider, since in an input sequence like "A
↓, B
↓" where A
and B
are tap-hold keys, A
and B
can't be settled yet.
But rethinking it, these keys will eventually settle, of course, depending on subsequent events. I've revised so that Chordal Hold considers input sequences involving multiple tap-hold keys and I'm happy how this is working out.
Details:
-
Consider an input sequence "
A
↓,B
↓,C
↓, ..." of all presses of tap-hold keys. If held until the tapping-term, then regardless of handedness, these keys should be settled as held as usual. It is important to preserve this behavior for home row mods so that it is possible to chord multiple mods, e.g. Ctrl + Shift, in one hand. -
Another case: consider if within the tapping term a key is released: "
A
↓,B
↓,C
↓,C
↑". Provided either Permissive Hold or Hold on Other Key Press is enabled, the tap-hold keysA
,B
precedingC
would then be settled as held. With Chordal Hold, perhaps depending on handedness some or all of the keys should be tapped instead.I've implemented a general heuristic (
waiting_buffer_find_chordal_hold_tap()
in the code) to decide this, based on evaluatingget_chordal_hold()
between successive pairs of keys. Some specific practical cases described for 3-key sequences, though the rule works for any number of keys:-
If
A
,B
,C
are all on the same hand (get_chordal_hold()
evaluates false betweenAB
andBC
), they are all considered tapped. This is effectively "typing streak" suppression of the hold function. This is cool! -
If
A
andB
are on one hand andC
on the other, then bothA
andB
are held. -
If
A
is on one hand andB
andC
on the other, then justA
is held.
-
-
Suppose an input sequence of tap-hold presses followed by a press of a regular key
Z
, like "A
↓,B
↓,C
↓, ...,Z
↓". Then, all the same logic as in case 2 applies.
The implementation is more invasive than where this PR started, which makes me nervous. The "state machine" is not easy to reason about or modify, though I'm starting to get a feel for it.
I think you understand this area of the code a lot better than I do. Please let me know whether what I describe here needs further explanation, or seems like a flawed design and/or could be implemented better. I've made an effort to make it clean, been testing Chordal Hold in daily use (typing with it as we speak), and significantly beefed up the unit tests. But surely there's room for improvement. Anyway, I'm excited how this is progressing! 🔥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading and changing the action_tapping.c
file after a long hiatus is non-trivial (to say the least) and I'm trying to get up to speed again to evaluate this PR.
My user space implementation is similar to ZMK's "positional hold-tap" when the following are pressed sequentially within tapping term on the right side of a QWERTY layout:
- Press RALT_T(KC_L)
- Press RGUI_T(KC_K)
- Continue holding both keys down
The host received the following:
KEY-DOWN - QMK: KC_L Event key: l Code: KeyL KeyCode: 76
KEY-DOWN - QMK: KC_RGUI Event key: Meta Code: MetaRight KeyCode: 93
But Chordal implementation does not override the first key and will transmit both modifiers:
KEY-DOWN - QMK: KC_RALT Event key: Alt Code: AltRight KeyCode: 18
KEY-DOWN - QMK: KC_RGUI Event key: Meta Code: MetaRight KeyCode: 93
Will review the changes in more detail to figure this out.
I haven't considered the scenario of more than 2 simultaneous key presses, and it seems that specific use cases will have nuances. This PR might benefit from more end user tests to ensure robustness because support of this feature will land on the Discord server when merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the comparison! Yes, that is intended that both mods be held. I just added a unit test to verify this case. This behavior is useful so that one hand can hold Alt + GUI, or some other set of mods, while the other hand types a hotkey or uses the mouse.
OTOH, I can imagine there are other ways to handle those use cases. When using behavior as in your first output, what is a good solution for sending hotkeys like Ctrl+Shift+V? Do you find such multi-mod hotkeys are rare enough in practice that it's not a big deal? or maybe switch over to a Callum-style mod scheme for such things, or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your solution includes the following user function:
bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
uint16_t other_keycode, keyrecord_t* other_record)
In the event of "Ctrl+Shift+V" or any other (frequent) same hand modifier combination, users can use that function to enable specific same hand modifier activation. Layouts such as Colemak that places a lot of frequently used letters on home row and may benefit from a default same-hand tap as default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point! You have convinced me. I use a layout with STRD NEIO
on the home row (a variant of Sturdy), and yes, defaulting to same-hand tap behavior among those keys is compelling.
I've revised the PR to have this behavior and (after adding a few exemptions to my get_chordal_hold()
) I like it a lot. Thank you for the suggestion!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making same-hand tap as default. Will provide feedback after testing this pull request as a daily driver.
tests/tap_hold_configurations/chordal_hold/permissive_hold/test_one_shot_keys.cpp
Show resolved
Hide resolved
Just as test feedback, I use this now daily as achordion replacement, works fine so far. |
This commit improves the heuristic used in generate-keyboard-c for inferring key handedness from keyboard.json geometry data. Heuristic summary: 1. If the layout is symmetric (e.g. most split keyboards), guess the handedness based on the sign of (x - layout_x_midpoint). 2. Otherwise, if the layout has a key of >=6u width, it is probably the spacebar. Form a dividing line through the spacebar, nearly vertical but with a slight angle to follow typical row stagger. 3. Otherwise, assume handedness based on the widest horizontal separation. I have tested this strategy on a couple dozen keyboards and found it to work reliably.
This PR adds "Chordal Hold," a tap-hold option implementing the "opposite hands" rule similar to Achordion and Bilateral Combinations. Chordal Hold may be used out of the box without any configuration, or if desired, Achordion-like per-chord configuration is supported.
Description
Suppose tap-hold key is pressed and then, before the tapping term, another key is pressed. With Chordal Hold, the tap-hold key is settled as tapped if the two keys are on the same hand. This behavior may be useful to avoid accidental modifier activation with mod-taps, particularly in rolled keypresses when using home row mods.
In the case that the keys are on opposite hands, Chordal Hold alone does not yet settle the tap-hold key. Chordal Hold may be used in combination with Hold On Other Key Press or Permissive Hold to determine the behavior. With Hold On Other Key Press, an opposite hands chord is settled immediately as held. Or with Permissive Hold, an opposite hands chord is settled as held provided the other key is pressed and released (nested press) before releasing the tap-hold key.
Further notes:
Chordal Hold has no effect after the tapping term.
Chordal Hold has no effect when combos are involved.
History
Previously, "opposite hands" rule behavior like this has been implemented in
This is a highly desired feature. Judging from Reddit, Achordion is my most popular QMK library. Still so far, "opposite hands" behavior like this is not yet in QMK core. I've been reluctant to touch action_tapping.c, it is an intimidating piece of code, but I think it's important to figure this out and make it happen!
Comparison to Achordion and Bilateral Combinations
Achordion and Bilateral Combinations operate after action_tapping, by manipulating hold events after QMK core has settled them. This means there are two stages of event buffering, first in core, then second in Achordion/Bilateral Combinations.
Chordal Hold operates within action_tapping itself. This is advantageous since one stage of event buffering rather than two can in some cases complete more quickly for reduced input lag, and it is (arguably) conceptually simpler.
How to use Chordal Hold
Chordal Hold is intended to be used together with either Permissive Hold or Hold On Other Keypress. Enable one of them, and enable Chordal Hold by adding in config.h:
For an Achordion-like experience, I suggest enabling Permissive Hold and setting the tapping term rather high, say, 250 ms. With Chordal Hold + Permissive Hold, keys usually settle before the tapping term.
"Handedness"
Determining whether keys are on the same or opposite hands involves defining the "handedness" of each key position. By default if nothing is specified, handedness is guessed based on keyboard geometry information from keyboard.json. If this is inaccurate, handedness may be specified by defining
chordal_hold_layout
. In keymap.c, definechordal_hold_layout
in the following form:Use the same
LAYOUT
macro as used to define your keymap layers. Each entry is a character, either'L'
for left,'R'
for right, or'*'
to exempt keys from the "opposite hands rule." When a key has'*'
handedness, pressing it with either hand results in a hold. This could be used perhaps on thumb keys or other places where you want to allow same-hand chords.chordal_hold_layout
is inspired by the convenient "BILATERAL_COMBINATIONS_HANDS_LAYER" in Bilateral Combinations. But with the differences thatchordal_hold_layout
is defined as a separate array outside the keymap and in char datatype to halve the flash space cost.Advanced configuration
Per-chord tuning of the behavior is possible with the
get_chordal_hold()
callback. This parallels Achordion's achordion_chord() callback. Returning true from this callback indicates that the chord may be settled as held, while returning false immediately settles the chord as tapped. Example:Please see the addition to
docs/tap_hold.md
for further documentation.Implementation
Inferring default handedness
By default if nothing is specified, handedness is guessed based on keyboard geometry information from keyboard.json. Under
"layout"
, the JSON has x-y coordinates for each key. Handedness is then inferred by the following heuristics (implemented in lib/python/qmk/cli/generate/keyboard_c.py):If the layout is symmetric (e.g. most split keyboards), guess the handedness based on the sign of
(x - layout_x_midpoint)
.Otherwise, if the layout has a key of >=6u width, it is probably the spacebar. Form a dividing line through the spacebar, nearly vertical but with a slight angle to follow typical row stagger.
Otherwise, assume handedness based on the widest horizontal separation.
I have tested this strategy on a couple dozen keyboards and found it to work reliably. Here are a few examples (legend: blue =
'L'
, red ='R'
, black ='*'
):Types of Changes
Checklist