diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index d02a2de3..92d60c93 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -34,6 +34,7 @@ enum flavor { FLAVOR_HOLD_PREFERRED, FLAVOR_BALANCED, FLAVOR_TAP_PREFERRED, + FLAVOR_TAP_POSITIONALLY_PREFERRED, }; enum status { @@ -73,7 +74,7 @@ struct active_hold_tap { struct k_delayed_work work; bool work_is_cancelled; - // initialized to -1, which is to be interpreted as "no other key has been pressed yet" + // Initialized to -1, which is to be interpreted as "no other key has been pressed yet". int32_t position_of_first_other_key_pressed; }; @@ -211,6 +212,8 @@ static struct active_hold_tap *store_hold_tap(uint32_t position, uint32_t param_ active_hold_taps[i].param_hold = param_hold; active_hold_taps[i].param_tap = param_tap; active_hold_taps[i].timestamp = timestamp; + + // Initialized to -1, which is to be interpreted as "no other key has been pressed yet". active_hold_taps[i].position_of_first_other_key_pressed = -1; return &active_hold_taps[i]; } @@ -277,6 +280,37 @@ static void decide_hold_preferred(struct active_hold_tap *hold_tap, enum decisio } } +static void decide_tap_positionally_preferred(struct active_hold_tap *hold_tap, + enum decision_moment event, + int32_t other_key_down_position) { + switch (event) { + case HT_KEY_UP: + hold_tap->status = STATUS_TAP; + return; + case HT_OTHER_KEY_DOWN: + hold_tap->status = is_other_key_trigger_key(hold_tap, other_key_down_position) ? STATUS_HOLD_INTERRUPT : STATUS_TAP; + return; + case HT_TIMER_EVENT: + hold_tap->status = STATUS_TAP; + return; + case HT_QUICK_TAP: + hold_tap->status = STATUS_TAP; + return; + default: + return; + } +} + +static bool is_other_key_trigger_key(struct active_hold_tap *hold_tap, int32_t other_key_down_position) { + for (int i = 0; i < hold_tap->config->hold_trigger_key_positions_len; i++) { + if (hold_tap->config->hold_trigger_key_positions[i] == + other_key_down_position) { + return true; + } + } + return false; +} + static inline const char *flavor_str(enum flavor flavor) { switch (flavor) { case FLAVOR_HOLD_PREFERRED: @@ -365,35 +399,13 @@ static int release_binding(struct active_hold_tap *hold_tap) { return behavior_keymap_binding_released(&binding, event); } -// Force a tap decision if the positional conditions for a hold decision are not met. -static void decide_positional_hold(struct active_hold_tap *hold_tap) { - // Check if the positional hold/tap feature is enabled. - if (!(hold_tap->config->hold_trigger_key_positions_len > 0)) { - return; - } - - // Only mutate the hold/tap decision if the first other key to be pressed - // (after the hold/tap key) is not one of the trigger keys. - if (!is_first_other_key_pressed_trigger_key(hold_tap)) { - return; - } - - // Since the positional key conditions have failed, force a TAP decision. - hold_tap->status = STATUS_TAP; -} - -static bool is_first_other_key_pressed_trigger_key(struct active_hold_tap *hold_tap) { - for (int i = 0; i < hold_tap->config->hold_trigger_key_positions_len; i++) { - if (hold_tap->config->hold_trigger_key_positions[i] == - hold_tap->position_of_first_other_key_pressed) { - return true; - } - } - return false; -} - static void decide_hold_tap(struct active_hold_tap *hold_tap, - enum decision_moment decision_moment) { + enum decision_moment decision_moment, + int32_t other_key_down_pos) { + if (decision_moment == HT_OTHER_KEY_DOWN && other_key_down_pos < 0) { + LOG_DBG("ERROR other_key_down_pos must be a valid key position if decision_moment == HT_OTHER_KEY_DOWN"); + } + if (hold_tap->status != STATUS_UNDECIDED) { return; } @@ -412,6 +424,8 @@ static void decide_hold_tap(struct active_hold_tap *hold_tap, decide_balanced(hold_tap, decision_moment); case FLAVOR_TAP_PREFERRED: decide_tap_preferred(hold_tap, decision_moment); + case FLAVOR_TAP_POSITIONALLY_PREFERRED: + decide_tap_positionally_preferred(hold_tap, decision_moment, other_key_down_pos); } } @@ -419,8 +433,6 @@ static void decide_hold_tap(struct active_hold_tap *hold_tap, return; } - decide_positional_hold(hold_tap); - // Since the hold-tap has been decided, clean up undecided_hold_tap and // execute the decided behavior. LOG_DBG("%d decided %s (%s decision moment %s)", hold_tap->position, @@ -578,7 +590,7 @@ static int position_state_changed_listener(const zmk_event_t *eh) { LOG_DBG("%d capturing %d %s event", undecided_hold_tap->position, ev->position, ev->state ? "down" : "up"); capture_event(eh); - decide_hold_tap(undecided_hold_tap, ev->state ? HT_OTHER_KEY_DOWN : HT_OTHER_KEY_UP); + decide_hold_tap(undecided_hold_tap, ev->state ? HT_OTHER_KEY_DOWN : HT_OTHER_KEY_UP, ev->state ? ev->position : -1); return ZMK_EV_EVENT_CAPTURED; } diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index 8e1443c6..bf20a5e3 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -26,6 +26,7 @@ We call this the 'hold-preferred' flavor of hold-taps. While this flavor may wor - The 'hold-preferred' flavor triggers the hold behavior when the `tapping-term-ms` has expired or another key is pressed. - The 'balanced' flavor will trigger the hold behavior when the `tapping-term-ms` has expired or another key is pressed and released. - The 'tap-preferred' flavor triggers the hold behavior when the `tapping-term-ms` has expired. It triggers the tap behavior when another key is pressed. +- The 'tap-positionally-preferred' flavor triggers the hold behavior if and only if a key at one of the `hold-trigger-key-positions` is pressed before `tapping-term-ms` expires. See the below section on `hold-trigger-key-positions` for more details. When the hold-tap key is released and the hold behavior has not been triggered, the tap behavior will trigger. @@ -59,6 +60,50 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin }; ``` +#### `hold-trigger-key-positions` + +- `hold-trigger-key-positions` is used exclusively with the `tap-positionally-preferred` flavor, and is required for the `tap-positionally-preferred` flavor. +- Specifies which key positions are eligible for triggering a hold behavior. +- If and only if a key at one of the `hold-trigger-key-positions` is pressed before `tapping-term-ms` expires, will 'tap-positionally-preferred' produce a hold behavior. +- `hold-trigger-key-positions` is an array of key positions indexes. Key positions are numbered / indexed according to the keys in your keymap, starting at 0. So, if the first key in your keymap is Q, this key is in position 0. The next key (possibly W) will have position 1, et cetera. +* See the following example: + +``` +#include +#include + +/ { + behaviors { + tpht: tap_positional_hold_tap { + compatible = "zmk,behavior-hold-tap"; + label = "TAP_POSITIONAL_HOLD_TAP"; + #binding-cells = <2>; + flavor = "tap-positionally-preferred"; + tapping-term-ms = <200>; + quick-tap-ms = <200>; + bindings = <&kp>, <&kp>; + hold-trigger-key-positions = <1>; // <---[[the W key]] + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + default_layer { + bindings = < + // position 0 position 1 position 2 + &tpht LEFT_SHIFT Q &kp W &kp E + >; + }; + }; +}; +``` + +- The sequence `(tpht_down, W_down, W_up, tpht_up)` produces `W`. The `tap-positionally-preferred` flavor produces a **hold** behavior because the first key pressed after the hold-tap key is the `W key`, which is in position 1, which **IS** included in `hold-trigger-key-positions`. +- Meanwhile the sequence `(tpht_down, E_down, E_up, tpht_up)` produces `qe`. The `tap-positionally-preferred` flavor produces a **tap** behavior because the first key pressed after the hold-tap key is the `E key`, which is in position 2, which is **NOT** included in `hold-trigger-key-positions`. +- If the `LEFT_SHIFT / Q key` is held by itself for longer than `tapping-term-ms`, a **tap** behavior is produced. +- The `tap-positionally-preferred` flavor is useful with home-row modifiers. With a home-row-modifier key in the left hand, by only including keys positions from the right hand in `hold-trigger-key-positions`, hold behaviors will only occur with cross-hand key combinations. + #### Home row mods This example configures a hold-tap that works well for homerow mods: @@ -95,52 +140,6 @@ This example configures a hold-tap that works well for homerow mods: If this config does not work for you, try the flavor "balanced" with a medium `tapping-term-ms` such as 200ms. -#### Positional hold-tap and `hold-trigger-key-positions` - -* Including `hold-trigger-key-postions` in your hold-tap behavior definition enables the positional hold-tap feature. -* With positional hold-tap enabled, your hold-tap behavior will only produce a hold behavior if, while the hold-tap key is still held down, the next key to be pressed is at one of the positions listed in the `hold-trigger-key-postions` array. -* `hold-trigger-key-postions` is an array of key positions indexes. Key positions are numbered / indexed according to the keys in your keymap, starting at 0. So, if the first key in your keymap is Q, this key is in position 0. The next key (possibly W) will have position 1, et cetera. -* See the following example, which references the below hold-tap behavior definition: - -``` -#include -#include - -/ { - behaviors { - pht: positional_hold_tap { - compatible = "zmk,behavior-hold-tap"; - label = "POSITIONAL_HOLD_TAP"; - #binding-cells = <2>; - flavor = "hold-preferred"; - tapping-term-ms = <400>; - quick-tap-ms = <200>; - bindings = <&kp>, <&kp>; - hold-trigger-key-positions = <1>; // <---[[the W key]] - }; - }; - - keymap { - compatible = "zmk,keymap"; - label ="Default keymap"; - default_layer { - bindings = < - // position 0 position 1 position 2 - &pht LEFT_SHIFT Q &kp W &kp E - >; - }; - }; -}; -``` - - * The sequence `(pht_down, W_down, W_up, E_down, E_up, pht_up)` produces `WE`. The positional hold-tap **IS** permitted to produce a hold behavior, because the first key pressed after the hold-tap key (i.e. the W key, in position 1) **IS** one of the `hold-trigger-key-positions`. - - * Meanwhile, the sequence `(pht_down, E_down, E_up, W_down, W_up pht_up)` produces `qew`. The positional hold-tap is **NOT** permitted to produce a hold behavior, because the first key pressed after the hold-tap key (i.e. the E key, in position 2) is **NOT** one of the `hold-trigger-key-positions`. - -* Positional hold-taps can be useful with home-row modifiers. By using a positional hold-tap behavior for home-row modifiers on the left hand, and setting `hold-trigger-key-positions` to the keys under the right hand, positional hold-tap will only allow hold behaviors to occur with cross-hand keypresses. - -* Note that for regular hold-tap behaviors a shorter `tapping-term` encourages hold decisions. However the opposite is true for positional hold-tap behaviors, where a shorter `tapping-term` actually encourages tap decisions. This is because when the `tapping-term` expires, this triggers the behavior to decide as either a tap or a hold. But if the user has not yet had time to press one of the `hold-trigger-key-positions`, then with positional hold-tap the behavior will decide as a tap. - #### Comparison to QMK The hold-preferred flavor works similar to the `HOLD_ON_OTHER_KEY_PRESS` setting in QMK. The 'balanced' flavor is similar to the `PERMISSIVE_HOLD` setting, and the `tap-preferred` flavor is similar to `IGNORE_MOD_TAP_INTERRUPT`.