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] Chordal Hold, an "opposite hands rule" tap-hold option similar to Achordion, Bilateral Combinations, and adapted for Vial. #90

Open
wants to merge 6 commits into
base: vial
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions data/mappings/info_config.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,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 @@ -385,7 +385,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 @@ -863,6 +867,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`
* Default: `false`
* `hold_on_other_key_press`
* Default: `false`
* `hold_on_other_key_press_per_key`
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`
* The height of the key, in key units.
* Default: `1` (1u)
* `hand`
* The handedness of the key for Chordal Hold, either `"L"` (left hand), `"R"` (right hand), or `"*"` (either or exempted handedness).
* `label`
* 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
163 changes: 163 additions & 0 deletions docs/tap_hold.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,169 @@ 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 when `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 when that
`KC_C` is released.

### 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 =
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.

::: 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` allows the chord to be 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.


## Retro Tapping

To enable `retro tapping`, add the following to your `config.h`:
Expand Down
1 change: 1 addition & 0 deletions keyboards/svalboard/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

#define SPLIT_TRANSACTION_IDS_KB KEYBOARD_SYNC_A

#define CHORDAL_HOLD
#define PERMISSIVE_HOLD
#define ACHORDION_STREAK
24 changes: 12 additions & 12 deletions keyboards/svalboard/info.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,18 @@
{"label": "l4s", "matrix": [4, 0], "x": 0.5, "y": 2.5, "h": 0.5},
{"label": "l4w", "matrix": [4, 4], "x": 0, "y": 1.5, "w": 0.5},
{"label": "l4ss", "matrix": [4, 5], "x": 0, "y": 1.5, "w": 0.5},
{"label": "rtd", "matrix": [5, 2], "x": 11, "y": 4, "h": 2},
{"label": "rti", "matrix": [5, 3], "x": 12, "y": 3.5, "w": 0.5, "h": 1.5},
{"label": "rtu", "matrix": [5, 4], "x": 12.5, "y": 3.5, "w": 0.5},
{"label": "rtuo", "matrix": [5, 1], "x": 10.5, "y": 3.5, "w": 0.5, "h": 1.5},
{"label": "rtlo", "matrix": [5, 0], "x": 10.5, "y": 5, "w": 0.5, "h": 1.5},
{"label": "rtdd", "matrix": [5, 5], "x": 11.5, "y": 5, "w": 0.5, "h": 1.5},
{"label": "ltd", "matrix": [0, 2], "x": 8.5, "y": 4, "h": 2},
{"label": "lti", "matrix": [0, 3], "x": 8, "y": 3.5, "w": 0.5, "h": 1.5},
{"label": "ltu", "matrix": [0, 4], "x": 7.5, "y": 3.5, "w": 0.5},
{"label": "ltuo", "matrix": [0, 1], "x": 9.5, "y": 3.5, "w": 0.5, "h": 1.5},
{"label": "ltlo", "matrix": [0, 0], "x": 9.5, "y": 5, "w": 0.5, "h": 1.5},
{"label": "ltdd", "matrix": [0, 5], "x": 8.5, "y": 5, "w": 0.5, "h": 1.5}
{"label": "rtd", "matrix": [5, 2], "x": 11, "y": 4, "h": 2, "hand": "*"},
{"label": "rti", "matrix": [5, 3], "x": 12, "y": 3.5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "rtu", "matrix": [5, 4], "x": 12.5, "y": 3.5, "w": 0.5, "hand": "*"},
{"label": "rtuo", "matrix": [5, 1], "x": 10.5, "y": 3.5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "rtlo", "matrix": [5, 0], "x": 10.5, "y": 5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "rtdd", "matrix": [5, 5], "x": 11.5, "y": 5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "ltd", "matrix": [0, 2], "x": 8.5, "y": 4, "h": 2, "hand": "*"},
{"label": "lti", "matrix": [0, 3], "x": 8, "y": 3.5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "ltu", "matrix": [0, 4], "x": 7.5, "y": 3.5, "w": 0.5, "hand": "*"},
{"label": "ltuo", "matrix": [0, 1], "x": 9.5, "y": 3.5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "ltlo", "matrix": [0, 0], "x": 9.5, "y": 5, "w": 0.5, "h": 1.5, "hand": "*"},
{"label": "ltdd", "matrix": [0, 5], "x": 8.5, "y": 5, "w": 0.5, "h": 1.5, "hand": "*"}
]
}
}
Expand Down
12 changes: 11 additions & 1 deletion keyboards/svalboard/keymaps/keymap_support.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ void mh_change_timeouts(void) {
write_eeprom_kb();
}

void toggle_chordal_hold(void) {
chordal_hold_toggle();
global_saved_values.enable_chordal_hold = chordal_hold_is_enabled();
write_eeprom_kb();
}

void toggle_achordion(void) {
global_saved_values.disable_achordion = !global_saved_values.disable_achordion;
write_eeprom_kb();
Expand Down Expand Up @@ -212,7 +218,7 @@ bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
// The keycodes below are all that are forced to drop you out of mouse mode.
// The intent is for this list to eventually become just KC_NO, and KC_TRNS
// as more functionality is exported to keybard, and those keys are removed
// from the firmmware. - ilc - 2024-10-05
// from the firmware. - ilc - 2024-10-05
#define BAD_KEYCODE_CONDITONAL (keycode == KC_NO || \
keycode == KC_TRNS || \
keycode == SV_LEFT_DPI_INC || \
Expand All @@ -221,6 +227,7 @@ bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
keycode == SV_RIGHT_DPI_DEC || \
keycode == SV_LEFT_SCROLL_TOGGLE || \
keycode == SV_RIGHT_SCROLL_TOGGLE || \
keycode == SV_TOGGLE_CHORDAL_HOLD || \
keycode == SV_TOGGLE_ACHORDION || \
keycode == SV_MH_CHANGE_TIMEOUTS)

Expand Down Expand Up @@ -281,6 +288,9 @@ bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
case SV_CAPS_WORD:
caps_word_toggle();
return false;
case SV_TOGGLE_CHORDAL_HOLD:
toggle_chordal_hold();
return false;
case SV_TOGGLE_ACHORDION:
toggle_achordion();
return false;
Expand Down
1 change: 1 addition & 0 deletions keyboards/svalboard/keymaps/keymap_support.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum my_keycodes {
SV_SCROLL_HOLD,
SV_SCROLL_TOGGLE,
SV_OUTPUT_STATUS,
SV_TOGGLE_CHORDAL_HOLD,
// New keycodes should go here, to avoid breaking existing keymaps - order must match vial.json
KC_NORMAL_HOLD = SAFE_RANGE,
KC_FUNC_HOLD,
Expand Down
5 changes: 5 additions & 0 deletions keyboards/svalboard/keymaps/vial/vial.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
"shortName": "Output\nStatus",
"title": "Output the current internal state of the keyboard.",
"name": "SV_OUTPUT_STATUS"
},
{
"shortName": "Toggle\nChordal",
"title": "Toggle Chordal Hold",
"name": "SV_TOGGLE_CHORDAL_HOLD"
}
],
"layouts": {
Expand Down
8 changes: 7 additions & 1 deletion keyboards/svalboard/svalboard.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ void read_eeprom_kb(void) {
write_eeprom_kb();
}
sval_active_layer = 0;
if (global_saved_values.enable_chordal_hold) {
chordal_hold_enable();
} else {
chordal_hold_disable();
}
}

static const char YES[] = "yes";
Expand All @@ -79,7 +84,8 @@ void output_keyboard_info(void) {
yes_or_no(global_saved_values.left_scroll), dpi_choices[global_saved_values.left_dpi_index],
yes_or_no(global_saved_values.right_scroll), dpi_choices[global_saved_values.right_dpi_index]);
send_string(output_buffer);
sprintf(output_buffer, "Achordion: %s, MH Keys Timer: %d\n",
sprintf(output_buffer, "Chordal Hold: %s, Achordion: %s, MH Keys Timer: %d\n",
yes_or_no(global_saved_values.enable_chordal_hold),
yes_or_no(!global_saved_values.disable_achordion),
mh_timer_choices[global_saved_values.mh_timer_index]);
send_string(output_buffer);
Expand Down
3 changes: 2 additions & 1 deletion keyboards/svalboard/svalboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ struct saved_values {
bool left_scroll :1;
bool right_scroll :1;
bool disable_achordion: 1;
unsigned int unused0 :5;
bool enable_chordal_hold: 1;
unsigned int unused0: 4;
uint8_t left_dpi_index;
uint8_t right_dpi_index;
uint8_t mh_timer_index;
Expand Down
Loading
Loading