Skip to content
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

Open
wants to merge 36 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fa857db
Chordal Hold: restrict what chords settle as hold
getreuer Nov 2, 2024
e0f648d
Chordal Hold: docs and further improvements
getreuer Nov 3, 2024
352f4fa
Fix formatting.
getreuer Nov 3, 2024
ed53b7d
Doc rewording and minor edit.
getreuer Nov 4, 2024
3fdbb07
Support Chordal Hold of multiple tap-hold keys.
getreuer Nov 8, 2024
99d49ac
Fix formatting.
getreuer Nov 8, 2024
da8ccf0
Simplification and additional test.
getreuer Nov 8, 2024
1606d67
Fix formatting.
getreuer Nov 8, 2024
bd7e54a
Tighten tests.
getreuer Nov 11, 2024
6b55824
Add test two_mod_taps_same_hand_hold_til_timeout.
getreuer Nov 14, 2024
e924a0c
Revise handing of pairs of tap-hold keys.
getreuer Nov 16, 2024
fb6c2d8
Generate a default chordal_hold_layout.
getreuer Nov 17, 2024
8f86425
Merge branch 'develop' into chordal_hold
getreuer Nov 17, 2024
5b5ff41
Document chordal_hold_handedness().
getreuer Nov 20, 2024
60e8288
Add license notice to new and branched files in PR.
getreuer Nov 23, 2024
4e46c16
Merge branch 'develop' into chordal_hold
getreuer Nov 23, 2024
a525048
Add `tapping.chordal_hold` property for info.json.
getreuer Nov 23, 2024
4f3f5b3
Update docs/reference_info_json.md
getreuer Nov 28, 2024
234fb97
Merge branch 'develop' into chordal_hold
getreuer Nov 28, 2024
2014205
Revise "hand" jsonschema.
getreuer Nov 28, 2024
355f9f9
Chordal Hold: Improved layout handedness heuristic.
getreuer Dec 5, 2024
788f4aa
Use Optional instead of `| None`.
getreuer Dec 5, 2024
da32973
Refactor to avoid lambdas.
getreuer Dec 5, 2024
ae9fa23
Merge branch 'develop' into chordal_hold
getreuer Dec 5, 2024
c4d9180
Remove trailing comma in chordal_hold_layout.
getreuer Dec 7, 2024
503752a
Minor docs edits.
getreuer Dec 13, 2024
5af4ae7
Merge branch 'develop' into chordal_hold
getreuer Dec 13, 2024
2b1eff2
Revise to allow combining multiple same-hand mods.
getreuer Dec 24, 2024
bb7a3c3
Merge branch 'develop' into chordal_hold
getreuer Dec 24, 2024
4c5f603
Fix formatting.
getreuer Dec 24, 2024
a2bbbfa
Merge branch 'develop' into chordal_hold
getreuer Jan 3, 2025
c0cf45c
Add a couple tests with LT keys.
getreuer Jan 3, 2025
36fdeb3
Remove stale use of CHORDAL_HOLD_LAYOUT.
getreuer Jan 8, 2025
2aacc99
Fix misspelling lastest -> latest
getreuer Jan 8, 2025
11cc997
Merge branch 'develop' into chordal_hold
getreuer Jan 8, 2025
e6ecff4
Merge branch 'develop' into chordal_hold
getreuer Jan 9, 2025
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
1 change: 1 addition & 0 deletions data/mappings/info_config.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
"SPLIT_WPM_ENABLE": {"info_key": "split.transport.sync.wpm", "value_type": "flag"},

// Tapping
"CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"},
"HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"},
"HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"},
"PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"},
Expand Down
7 changes: 6 additions & 1 deletion data/schemas/keyboard.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,11 @@
"h": {"$ref": "qmk.definitions.v1#/key_unit"},
"w": {"$ref": "qmk.definitions.v1#/key_unit"},
"x": {"$ref": "qmk.definitions.v1#/key_unit"},
"y": {"$ref": "qmk.definitions.v1#/key_unit"}
"y": {"$ref": "qmk.definitions.v1#/key_unit"},
"hand": {
"type": "string",
"enum": ["L", "R", "*"]
}
}
}
}
Expand Down Expand Up @@ -915,6 +919,7 @@
"tapping": {
"type": "object",
"properties": {
"chordal_hold": {"type": "boolean"},
"force_hold": {"type": "boolean"},
"force_hold_per_key": {"type": "boolean"},
"ignore_mod_tap_interrupt": {"type": "boolean"},
Expand Down
4 changes: 4 additions & 0 deletions docs/reference_info_json.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ You can create `info.json` files at every level under `qmk_firmware/keyboards/<k
* The delay between keydown and keyup for tap events in milliseconds.
* Default: `0` (no delay)
* `tapping`
* `chordal_hold` <Badge type="info">Boolean</Badge>
* Default: `false`
* `hold_on_other_key_press` <Badge type="info">Boolean</Badge>
* Default: `false`
* `hold_on_other_key_press_per_key` <Badge type="info">Boolean</Badge>
Expand Down Expand Up @@ -328,6 +330,8 @@ The ISO enter key is represented by a 1.25u×2uh key. Renderers which utilize in
* `h` <Badge type="info">KeyUnit</Badge>
* The height of the key, in key units.
* Default: `1` (1u)
* `hand` <Badge type="info">String</Badge>
* The handedness of the key for Chordal Hold, either `"L"` (left hand), `"R"` (right hand), or `"*"` (either or exempted handedness).
* `label` <Badge type="info">String</Badge>
* What to name the key. This is *not* a key assignment as in the keymap, but should usually correspond to the keycode for the first layer of the default keymap.
* Example: `"Escape"`
Expand Down
242 changes: 242 additions & 0 deletions docs/tap_hold.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,248 @@ uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) {
If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`.
:::

## Chordal Hold

Chordal Hold is intended to be used together with either Permissive Hold or Hold
On Other Key Press. Chordal Hold is enabled by adding to your `config.h`:

```c
#define CHORDAL_HOLD
```

Chordal Hold implements, by default, an "opposite hands" rule. Suppose a
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.

Otherwise, if the keys are on opposite hands, Chordal Hold introduces no new
behavior. Hold On Other Key Press or Permissive Hold may be used together with
Chordal Hold to configure the behavior in the opposite hands case. 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.

Chordal Hold may be useful to avoid accidental modifier activation with
mod-taps, particularly in rolled keypresses when using home row mods.

Notes:

* Chordal Hold has no effect after the tapping term.

* Combos are exempt from the opposite hands rule, since "handedness" is
ill-defined in this case. Even so, Chordal Hold's behavior involving combos
may be customized through the `get_chordal_hold()` callback.

An example of a sequence that is affected by “chordal hold”:

- `SFT_T(KC_A)` Down
- `KC_C` Down
- `KC_C` Up
- `SFT_T(KC_A)` Up

```
TAPPING_TERM
+---------------------------|--------+
| +----------------------+ | |
| | SFT_T(KC_A) | | |
| +----------------------+ | |
| +--------------+ | |
| | KC_C | | |
| +--------------+ | |
+---------------------------|--------+
```

If the two keys are on the same hand, then this will produce `ac` with
`SFT_T(KC_A)` settled as tapped the moment that `KC_C` is pressed.

If the two keys are on opposite hands and the `HOLD_ON_OTHER_KEY_PRESS`
option enabled, this will produce `C` with `SFT_T(KC_A)` settled as held the
moment that `KC_C` is pressed.

Or if the two keys are on opposite hands and the `PERMISSIVE_HOLD` option is
enabled, this will produce `C` with `SFT_T(KC_A)` settled as held the
moment that `KC_C` is released.
getreuer marked this conversation as resolved.
Show resolved Hide resolved

### Chordal Hold 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.

Handedness may be specified with `chordal_hold_layout`. In keymap.c, define
`chordal_hold_layout` in the following form:

```c
const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM =
getreuer marked this conversation as resolved.
Show resolved Hide resolved
LAYOUT(
'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R',
'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R',
'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R',
'L', 'L', 'L', 'R', 'R', 'R'
);
```

Use the same `LAYOUT` macro as used to define your keymap layers. Each entry is
a character indicating the handedness of one key, either `'L'` for left, `'R'`
for right, or `'*'` to exempt keys from the "opposite hands rule." A key with
`'*'` handedness may settle as held in chords with any other key. This could be
used perhaps on thumb keys or other places where you want to allow same-hand
chords.

Keyboard makers may specify handedness in keyboard.json. Under `"layouts"`,
specify the handedness of a key by adding a `"hand"` field with a value of
either `"L"`, `"R"`, or `"*"`. Note that if `"layouts"` contains multiple
layouts, only the first one is read. For example:

```json
{"matrix": [5, 6], "x": 0, "y": 5.5, "w": 1.25, "hand", "*"},
```

Alternatively, handedness may be defined functionally with
`chordal_hold_handedness()`. For example, in keymap.c define:

```c
char chordal_hold_handedness(keypos_t key) {
if (key.col == 0 || key.col == MATRIX_COLS - 1) {
return '*'; // Exempt the outer columns.
}
// On split keyboards, typically, the first half of the rows are on the
// left, and the other half are on the right.
return key.row < MATRIX_ROWS / 2 ? 'L' : 'R';
}
```

Given the matrix position of a key, the function should return `'L'`, `'R'`, or
`'*'`. Adapt the logic in this function according to the keyboard's matrix.

getreuer marked this conversation as resolved.
Show resolved Hide resolved
::: warning
Note the matrix may have irregularities around larger keys, around the edges of
the board, and around thumb clusters. You may find it helpful to use [this
debugging example](faq_debug#which-matrix-position-is-this-keypress) to
correspond physical keys to matrix positions.
:::

::: tip If you define both `chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS]` and
`chordal_hold_handedness(keypos_t key)` for handedness, the latter takes
precedence.


### Per-chord customization

Beyond the per-key configuration possible through handedness, Chordal Hold may
be configured at a *per-chord* granularity for detailed tuning. In keymap.c,
define `get_chordal_hold()`. Returning true settles the chord as held, while
returning false settles as tapped.

For example:

```c
bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
uint16_t other_keycode, keyrecord_t* other_record) {
// Exceptionally allow some one-handed chords for hotkeys.
switch (tap_hold_keycode) {
case LCTL_T(KC_Z):
if (other_keycode == KC_C || other_keycode == KC_V) {
return true;
}
break;

case RCTL_T(KC_SLSH):
if (other_keycode == KC_N) {
return true;
}
break;
}
// Otherwise defer to the opposite hands rule.
return get_chordal_hold_default(tap_hold_record, other_record);
}
```

As shown in the last line above, you may use
`get_chordal_hold_default(tap_hold_record, other_record)` to get the default tap
vs. hold decision according to the opposite hands rule.

If you use home row mods, you may want to produce a hotkey like Ctrl+Shift+V by
holding Ctrl and Shift mod-taps on one hand while tapping `KC_V` with the other
hand, say:

- `RCTL_T(KC_K)` Down
- `RSFT_T(KC_L)` Down (on the same hand as `RCTL_T(KC_K)`)
- `KC_V` Down
- `KC_V` Up
- `RCTL_T(KC_K)` Up
- `RSFT_T(KC_L)` Up

However, supposing `RCTL_T(KC_K)` and `RSFT_T(KC_L)` are on the same hand,
Chordal Hold by default considers `RCTL_T(KC_K)` tapped, producing "`kV`"
instead of the desired Ctrl+Shift+V.

To address this, `get_chordal_hold()` may be defined to allow chords between any
pair of mod-tap keys with

```c
bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
uint16_t other_keycode, keyrecord_t* other_record) {
// Allow hold between any pair of mod-tap keys.
if (IS_QK_MOD_TAP(tap_hold_keycode) && IS_QK_MOD_TAP(other_keycode)) {
return true;
}

// Otherwise defer to the opposite hands rule.
return get_chordal_hold_default(tap_hold_record, other_record);
}
```

Or to allow one-handed chords of specific mod-taps but not others, use:

```c
bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
uint16_t other_keycode, keyrecord_t* other_record) {
switch (tap_hold_keycode) {
case RCTL_T(KC_K):
if (other_keycode == RSFT_T(KC_L)) {
// Allow hold in "RCTL_T(KC_K) down, RSFT_T(KC_L) down".
return true;
}
break;

case RSFT_T(KC_L):
if (other_keycode == RCTL_T(KC_K)) {
// Allow hold in "RSFT_T(KC_L) down, RCTL_T(KC_K) down".
return true;
}
break;
}
// Otherwise defer to the opposite hands rule.
return get_chordal_hold_default(tap_hold_record, other_record);
}
```

Above, two exceptions are defined, one where `RCTL_T(KC_K)` is pressed first and
another where `RSFT_T(KC_L)` is held first, such that Ctrl+Shift+V could be done
by holding the mod-taps in either order. For yet finer control, you could choose
to define an exception for one order but not the other:

```c
bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
uint16_t other_keycode, keyrecord_t* other_record) {
switch (tap_hold_keycode) {
case RCTL_T(KC_K):
if (other_keycode == RSFT_T(KC_L)) {
// Allow hold in "RCTL_T(KC_K) down, RSFT_T(KC_L), down".
return true;
}
break;

// ... but RSFT_T(KC_L) is considered tapped in
// "RSFT_T(KC_L) down, RCTL_T(KC_K) down".
}
// Otherwise defer to the opposite hands rule.
return get_chordal_hold_default(tap_hold_record, other_record);
}
```


## Retro Tapping

To enable `retro tapping`, add the following to your `config.h`:
Expand Down
Loading
Loading