From ed8497bda04ecf8a4d8bdee37d5638a6630c3575 Mon Sep 17 00:00:00 2001 From: Nick Conway Date: Fri, 2 Jun 2023 20:56:24 -0400 Subject: [PATCH 1/3] Retro tap binding --- .../behaviors/zmk,behavior-hold-tap.yaml | 9 + app/src/behaviors/behavior_hold_tap.c | 18 +- docs/docs/behaviors/hold-tap.md | 346 ++++++++++++++++++ 3 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 docs/docs/behaviors/hold-tap.md diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index 76f14d12d..0742673fc 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -43,6 +43,15 @@ properties: type: boolean retro-tap: type: boolean + retro-tap-behavior: + type: string + default: "" + retro-tap-param1: + type: int + default: 0 + retro-tap-param2: + type: int + default: 0 hold-trigger-key-positions: type: array required: false diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 3df3bc864..0a9d28da7 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -39,6 +39,7 @@ enum flavor { enum status { STATUS_UNDECIDED, STATUS_TAP, + STATUS_RETRO_TAP, STATUS_HOLD_INTERRUPT, STATUS_HOLD_TIMER, }; @@ -62,6 +63,9 @@ struct behavior_hold_tap_config { bool hold_while_undecided; bool hold_while_undecided_linger; bool retro_tap; + char *retro_tap_behavior; + uint32_t retro_tap_param1; + uint32_t retro_tap_param2; bool hold_trigger_on_release; int32_t hold_trigger_key_positions_len; int32_t hold_trigger_key_positions[]; @@ -470,6 +474,8 @@ static int press_binding(struct active_hold_tap *hold_tap) { } else { return press_hold_binding(hold_tap); } + } else if (hold_tap->status == STATUS_RETRO_TAP) { + store_last_hold_tapped(hold_tap); } else { if (hold_tap->config->hold_while_undecided && !hold_tap->config->hold_while_undecided_linger) { @@ -487,6 +493,9 @@ static int release_binding(struct active_hold_tap *hold_tap) { if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { return release_hold_binding(hold_tap); + } else if (hold_tap->status == STATUS_RETRO_TAP) { + return press_retro_tap_binding(hold_tap); + return release_retro_tap_binding(hold_tap); } else { return release_tap_binding(hold_tap); } @@ -581,7 +590,11 @@ static void decide_retro_tap(struct active_hold_tap *hold_tap) { if (hold_tap->status == STATUS_HOLD_TIMER) { release_binding(hold_tap); LOG_DBG("%d retro tap", hold_tap->position); - hold_tap->status = STATUS_TAP; + if (strcmp(hold_tap->config->retro_tap_behavior, "") == 0) { + hold_tap->status = STATUS_TAP; + } else { + hold_tap->status = STATUS_RETRO_TAP; + } press_binding(hold_tap); return; } @@ -870,6 +883,9 @@ static int behavior_hold_tap_init(const struct device *dev) { .hold_while_undecided = DT_INST_PROP(n, hold_while_undecided), \ .hold_while_undecided_linger = DT_INST_PROP(n, hold_while_undecided_linger), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ + .retro_tap_behavior = DT_INST_PROP(n, retro_tap_behavior), \ + .retro_tap_param1 = DT_INST_PROP(n, retro_tap_param1), \ + .retro_tap_param2 = DT_INST_PROP(n, retro_tap_param2), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ .hold_trigger_key_positions = DT_INST_PROP(n, hold_trigger_key_positions), \ .hold_trigger_key_positions_len = DT_INST_PROP_LEN(n, hold_trigger_key_positions), \ diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md new file mode 100644 index 000000000..6c4a126af --- /dev/null +++ b/docs/docs/behaviors/hold-tap.md @@ -0,0 +1,346 @@ +--- +title: Hold-Tap Behavior +sidebar_label: Hold-Tap +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Summary + +Hold-tap is the basis for other behaviors such as layer-tap and mod-tap. + +Simply put, the hold-tap key will output the 'hold' behavior if it's held for a while, and output the 'tap' behavior when it's tapped quickly. + +### Hold-Tap + +The graph below shows how the hold-tap decides between a 'tap' and a 'hold'. + +![Simple behavior](../assets/hold-tap/case1_2.svg) + +By default, the hold-tap is configured to also select the 'hold' functionality if another key is tapped while it's active: + +![Hold preferred behavior](../assets/hold-tap/case_hold_preferred.svg) + +We call this the 'hold-preferred' flavor of hold-taps. While this flavor may work very well for a ctrl/escape key, it's not very well suited for home-row mods or layer-taps. That's why there are two more flavors to choose from: 'tap-preferred' and 'balanced'. + +#### Flavors + +- 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. Pressing another key within `tapping-term-ms` does not affect the decision. +- The 'tap-unless-interrupted' flavor triggers a hold behavior only when another key is pressed before `tapping-term-ms` has expired. It triggers the tap behavior in all other situations. + +When the hold-tap key is released and the hold behavior has not been triggered, the tap behavior will trigger. + +![Hold-tap comparison](../assets/hold-tap/comparison.svg) + +### Basic usage + +For basic usage, please see the [mod-tap](mod-tap.md) and [layer-tap](layers.md#layer-tap) pages. + +### Advanced Configuration + +#### `tapping-term-ms` + +Defines how long a key must be pressed to trigger Hold behavior. + +#### `quick-tap-ms` + +If you press a tapped hold-tap again within `quick-tap-ms` milliseconds, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). + +#### `global-quick-tap` + +If `global-quick-tap` is enabled, then `quick-tap-ms` will apply not only when the given hold-tap is tapped, but for any key tapped before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. + +For example, the following hold-tap configuration enables `global-quick-tap` with a 125 millisecond `quick-tap-ms` term. + +``` +gqt: global-quick-tap { + compatible = "zmk,behavior-hold-tap"; + label = "GLOBAL_QUICK_TAP"; + #binding-cells = <2>; + flavor = "tap-preferred"; + tapping-term-ms = <200>; + quick-tap-ms = <125>; + global-quick-tap; + bindings = <&kp>, <&kp>; +}; +``` + +If you press `&kp A` and then `&gqt LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `quick-tap-ms`. This quick-tap behavior will work for any key press, whether it is within a behavior like hold-tap, or a simple `&kp`. This means the `&gqt LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** a key press. + +Note that the greater the value of `quick-tap-ms` is, the harder it will be to invoke the hold behavior, making this feature less applicable for use-cases like capitalizing letters while typing normally. However, if the hold behavior isn't used during fast typing, then it can be an effective way to mitigate misfires. + +#### `retro-tap` + +If `retro-tap` is enabled, the tap behavior is triggered when releasing the hold-tap key if no other key was pressed in the meantime. + +For example, if you press `&mt LEFT_SHIFT A` and then release it without pressing another key, it will output `a`. + +``` +&mt { + retro-tap; +}; +``` + +To define a behavior to use instead of the tap behavior, include `retro-tap-behavior`, `retro-tap-param1`, and/or `retro-tap-param2` in your hold-tap definition. + +- `retro-tap-behavior` refers to the label string of the desired behavior. See below for a list of these label strings for built-in behaviors. +- `retro-tap-param1` refers to the first thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, LSHFT will be param1. +- `retro-tap-param2` refers to the second thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, A will be param2. + +List of built-in behaviors and their corresponding label strings: + +- &bl - "BCKLGHT" +- &bt - "BLUETOOTH" +- &caps_word - "CAPS_WORD" +- &ext_power - "EXTPOWER" +- &gresc - "GRAVE_ESCAPE" +- &kp - "KEY_PRESS" +- &key_repeat - "KEY_REPEAT" +- &none - "NONE" +- &out - "OUTPUTS" +- &reset - "RESET" +- &bootloader - "BOOTLOAD" +- &rgb_ug - "RGB_UG" +- &sk - "STICKY_KEY" +- &sl - "STICKY_LAYER" +- &to - "TO_LAYER" +- &tog - "TOGGLE_LAYER" +- &trans - "TRANS" + +#### Positional hold-tap and `hold-trigger-key-positions` + +Including `hold-trigger-key-positions` in your hold-tap definition turns on the positional hold-tap feature. With positional hold-tap enabled, if you press any key **NOT** listed in `hold-trigger-key-positions` before `tapping-term-ms` expires, it will produce a tap. + +In all other situations, positional hold-tap will not modify the behavior of your hold-tap. Positional hold-tap is useful when used with home-row modifiers: for example, if you have a home-row modifier key in the left hand, by including only key positions from the right hand in `hold-trigger-key-positions`, you will only get hold behaviors during cross-hand key combinations. + +:::info +Note that `hold-trigger-key-positions` is an array of key position indexes. Key positions are numbered sequentially according to your keymap, starting with 0. So if the first key in your keymap is Q, this key is in position 0. The next key (probably W) will be in position 1, et cetera. +::: + +See the following example, which uses a hold-tap behavior definition, configured with the `hold-preferred` flavor, and with positional hold-tap enabled: + +``` +#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, E_down, E_up, pht_up)` produces `qe`. The normal hold behavior (LEFT_SHIFT) **IS** modified into a tap behavior (Q) by positional hold-tap 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`. +- The sequence `(pht_down, W_down, W_up, pht_up)` produces `W`. The normal hold behavior (LEFT_SHIFT) **is NOT** modified into a tap behavior (Q) by positional hold-tap 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`. +- If the `LEFT_SHIFT / Q key` is held by itself for longer than `tapping-term-ms`, a hold behavior is produced. This is because positional hold-tap only modifies the behavior of a hold-tap if another key is pressed before the `tapping-term-ms` period expires. + +By default, `hold-trigger-key-positions` are evaluated upon the first _key press_ after +the hold-tap. For homerow mods, this is not always ideal, because it prevents combining multiple modifiers unless they are included in `hold-trigger-key-positions`. To overwrite this behavior, one can set `hold-trigger-on-release`. If set to true, the evaluation of `hold-trigger-key-positions` gets delayed until _key release_. This allows combining multiple modifiers when the next key is _held_, while still deciding the hold-tap in favor of a tap when the next key is _tapped_. + +### Example Use-Cases + + + + + +The following are suggested hold-tap configurations that work well with home row mods: + +##### Option 1: cross-hand only modifiers, using `tap-unless-interrupted` and positional hold-tap (`hold-trigger-key-positions`) + +```dtsi title="Homerow Mods: Cross-hand Example" +#include +#include + +/ { + behaviors { + lh_pht: left_hand_positional_hold_tap { + compatible = "zmk,behavior-hold-tap"; + label = "LEFT_POSITIONAL_HOLD_TAP"; + #binding-cells = <2>; + flavor = "tap-unless-interrupted"; + tapping-term-ms = <100>; // <---[[produces tap if held longer than tapping-term-ms]] + quick-tap-ms = <200>; + bindings = <&kp>, <&kp>; + hold-trigger-key-positions = <5 6 7 8 9 10>; // <---[[right-hand keys]] + }; + }; + + keymap { + compatible = "zmk,keymap"; + default_layer { + bindings = < + // position 0 pos 1 pos 2 pos 3 pos 4 pos 5 pos 6 pos 7 pos 8 pos 9 pos 10 + &lh_pht LSFT A &lh_pht LGUI S &lh_pht LALT D &lh_pht LCTL F &kp G &kp H &kp I &kp J &kp K &kp L &kp SEMI + >; + }; + }; +}; +``` + +##### Option 2: `tap-preferred` + +```dtsi title="Homerow Mods: Tap-Preferred Example" +#include +#include + +/ { + behaviors { + hm: homerow_mods { + compatible = "zmk,behavior-hold-tap"; + label = "HOMEROW_MODS"; + #binding-cells = <2>; + tapping-term-ms = <150>; + quick-tap-ms = <0>; + flavor = "tap-preferred"; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + default_layer { + bindings = < + &hm LCTRL A &hm LGUI S &hm LALT D &hm LSHIFT F + >; + }; + }; +}; +``` + +##### Option 3: `balanced` + +```dtsi title="Homerow Mods: Balanced Example" +#include +#include + +/ { + behaviors { + bhm: balanced_homerow_mods { + compatible = "zmk,behavior-hold-tap"; + label = "HOMEROW_MODS"; + #binding-cells = <2>; + tapping-term-ms = <200>; // <---[[moderate duration]] + quick-tap-ms = <0>; + flavor = "balanced"; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + default_layer { + bindings = < + &bhm LCTRL A &bhm LGUI S &bhm LALT D &bhm LSHIFT F + >; + }; + }; +}; +``` + + + + + +A popular method of implementing Autoshift in ZMK involves a C-preprocessor macro, commonly defined as `AS(keycode)`. This macro applies the `LSHIFT` modifier to the specified `keycode` when `AS(keycode)` is held, and simply performs a [keypress](key-press.md), `&kp keycode`, when the `AS(keycode)` binding is tapped. This simplifies the use of Autoshift in a keymap, as the complete hold-tap bindings for each desired Autoshift key, as in `&as LS() &as LS() ... &as LS() `, can be quite cumbersome to use when applied to a large portion of the keymap. + +```dtsi title="Hold-Tap Example: Autoshift" +#include +#include + +#define AS(keycode) &as LS(keycode) keycode // Autoshift Macro + +/ { + behaviors { + as: auto_shift { + compatible = "zmk,behavior-hold-tap"; + label = "AUTO_SHIFT"; + #binding-cells = <2>; + tapping_term_ms = <135>; + quick_tap_ms = <0>; + flavor = "tap-preferred"; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + default_layer { + bindings = < + AS(Q) AS(W) AS(E) AS(R) AS(T) AS(Y) // Autoshift applied for QWERTY keys + >; + }; + }; +}; +``` + + + + + +This hold-tap example implements a [momentary-layer](layers.md/#momentary-layer) when the keybind is held and a [toggle-layer](layers.md/#toggle-layer) when it is tapped. Similar to the Autoshift and Sticky Hold use-cases, a `MO_TOG(layer)` macro is defined such that the `&mo` and `&tog` behaviors can target a single layer. + +```dtsi title="Hold-Tap Example: Momentary layer on Hold, Toggle layer on Tap" +#include +#include + +#define MO_TOG(layer) &mo_tog layer layer // Macro to apply momentary-layer-on-hold/toggle-layer-on-tap to a specific layer + +/ { + behaviors { + mo_tog: behavior_mo_tog { + compatible = "zmk,behavior-hold-tap"; + label = "mo_tog"; + #binding-cells = <2>; + flavor = "hold-preferred"; + tapping-term-ms = <200>; + bindings = <&mo>, <&tog>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + default_layer { + bindings = < + &mo_tog 2 1 // &mo 2 on hold, &tog 1 on tap + MO_TOG(3) // &mo 3 on hold, &tog 3 on 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 the QMK default. From a7ab7bad9e9847f2ab47d2cdb679c21ced6089c9 Mon Sep 17 00:00:00 2001 From: Nick Conway Date: Tue, 17 Jun 2025 15:31:12 -0400 Subject: [PATCH 2/3] fix(retro-tap-binding): fix retro tap binding on new version --- app/src/behaviors/behavior_hold_tap.c | 41 ++- docs/docs/behaviors/hold-tap.md | 346 ----------------------- docs/docs/keymaps/behaviors/hold-tap.mdx | 26 ++ 3 files changed, 63 insertions(+), 350 deletions(-) delete mode 100644 docs/docs/behaviors/hold-tap.md diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 0a9d28da7..9a316e1fc 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -63,7 +63,7 @@ struct behavior_hold_tap_config { bool hold_while_undecided; bool hold_while_undecided_linger; bool retro_tap; - char *retro_tap_behavior; + char *retro_tap_behavior_dev; uint32_t retro_tap_param1; uint32_t retro_tap_param2; bool hold_trigger_on_release; @@ -434,6 +434,21 @@ static int press_tap_binding(struct active_hold_tap *hold_tap) { return zmk_behavior_invoke_binding(&binding, event, true); } +static int press_retro_tap_binding(struct active_hold_tap *hold_tap) { + struct zmk_behavior_binding_event event = { + .position = hold_tap->position, + .timestamp = hold_tap->timestamp, +#if IS_ENABLED(CONFIG_ZMK_SPLIT) + .source = hold_tap->source, +#endif + }; + + struct zmk_behavior_binding binding = {.behavior_dev = hold_tap->config->retro_tap_behavior_dev, + .param1 = hold_tap->config->retro_tap_param1, + .param2 = hold_tap->config->retro_tap_param2}; + return zmk_behavior_invoke_binding(&binding, event, true); +} + static int release_hold_binding(struct active_hold_tap *hold_tap) { struct zmk_behavior_binding_event event = { .position = hold_tap->position, @@ -462,6 +477,21 @@ static int release_tap_binding(struct active_hold_tap *hold_tap) { return zmk_behavior_invoke_binding(&binding, event, false); } +static int release_retro_tap_binding(struct active_hold_tap *hold_tap) { + struct zmk_behavior_binding_event event = { + .position = hold_tap->position, + .timestamp = hold_tap->timestamp, +#if IS_ENABLED(CONFIG_ZMK_SPLIT) + .source = hold_tap->source, +#endif + }; + + struct zmk_behavior_binding binding = {.behavior_dev = hold_tap->config->retro_tap_behavior_dev, + .param1 = hold_tap->config->retro_tap_param1, + .param2 = hold_tap->config->retro_tap_param2}; + return zmk_behavior_invoke_binding(&binding, event, false); +} + static int press_binding(struct active_hold_tap *hold_tap) { if (hold_tap->config->retro_tap && hold_tap->status == STATUS_HOLD_TIMER) { return 0; @@ -476,6 +506,7 @@ static int press_binding(struct active_hold_tap *hold_tap) { } } else if (hold_tap->status == STATUS_RETRO_TAP) { store_last_hold_tapped(hold_tap); + return 0; } else { if (hold_tap->config->hold_while_undecided && !hold_tap->config->hold_while_undecided_linger) { @@ -494,7 +525,7 @@ static int release_binding(struct active_hold_tap *hold_tap) { if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { return release_hold_binding(hold_tap); } else if (hold_tap->status == STATUS_RETRO_TAP) { - return press_retro_tap_binding(hold_tap); + press_retro_tap_binding(hold_tap); return release_retro_tap_binding(hold_tap); } else { return release_tap_binding(hold_tap); @@ -587,13 +618,15 @@ static void decide_retro_tap(struct active_hold_tap *hold_tap) { if (!hold_tap->config->retro_tap) { return; } + if (hold_tap->status == STATUS_HOLD_TIMER) { release_binding(hold_tap); LOG_DBG("%d retro tap", hold_tap->position); - if (strcmp(hold_tap->config->retro_tap_behavior, "") == 0) { + if (strcmp(hold_tap->config->retro_tap_behavior_dev, "") == 0) { hold_tap->status = STATUS_TAP; } else { hold_tap->status = STATUS_RETRO_TAP; + LOG_DBG("%d RETRO TAP", hold_tap->position); } press_binding(hold_tap); return; @@ -883,7 +916,7 @@ static int behavior_hold_tap_init(const struct device *dev) { .hold_while_undecided = DT_INST_PROP(n, hold_while_undecided), \ .hold_while_undecided_linger = DT_INST_PROP(n, hold_while_undecided_linger), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ - .retro_tap_behavior = DT_INST_PROP(n, retro_tap_behavior), \ + .retro_tap_behavior_dev = DT_INST_PROP(n, retro_tap_behavior), \ .retro_tap_param1 = DT_INST_PROP(n, retro_tap_param1), \ .retro_tap_param2 = DT_INST_PROP(n, retro_tap_param2), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md deleted file mode 100644 index 6c4a126af..000000000 --- a/docs/docs/behaviors/hold-tap.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: Hold-Tap Behavior -sidebar_label: Hold-Tap ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Summary - -Hold-tap is the basis for other behaviors such as layer-tap and mod-tap. - -Simply put, the hold-tap key will output the 'hold' behavior if it's held for a while, and output the 'tap' behavior when it's tapped quickly. - -### Hold-Tap - -The graph below shows how the hold-tap decides between a 'tap' and a 'hold'. - -![Simple behavior](../assets/hold-tap/case1_2.svg) - -By default, the hold-tap is configured to also select the 'hold' functionality if another key is tapped while it's active: - -![Hold preferred behavior](../assets/hold-tap/case_hold_preferred.svg) - -We call this the 'hold-preferred' flavor of hold-taps. While this flavor may work very well for a ctrl/escape key, it's not very well suited for home-row mods or layer-taps. That's why there are two more flavors to choose from: 'tap-preferred' and 'balanced'. - -#### Flavors - -- 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. Pressing another key within `tapping-term-ms` does not affect the decision. -- The 'tap-unless-interrupted' flavor triggers a hold behavior only when another key is pressed before `tapping-term-ms` has expired. It triggers the tap behavior in all other situations. - -When the hold-tap key is released and the hold behavior has not been triggered, the tap behavior will trigger. - -![Hold-tap comparison](../assets/hold-tap/comparison.svg) - -### Basic usage - -For basic usage, please see the [mod-tap](mod-tap.md) and [layer-tap](layers.md#layer-tap) pages. - -### Advanced Configuration - -#### `tapping-term-ms` - -Defines how long a key must be pressed to trigger Hold behavior. - -#### `quick-tap-ms` - -If you press a tapped hold-tap again within `quick-tap-ms` milliseconds, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled). - -#### `global-quick-tap` - -If `global-quick-tap` is enabled, then `quick-tap-ms` will apply not only when the given hold-tap is tapped, but for any key tapped before it. This effectively disables the hold-tap when typing quickly, which can be quite useful for homerow mods. It can also have the effect of removing the input delay when typing quickly. - -For example, the following hold-tap configuration enables `global-quick-tap` with a 125 millisecond `quick-tap-ms` term. - -``` -gqt: global-quick-tap { - compatible = "zmk,behavior-hold-tap"; - label = "GLOBAL_QUICK_TAP"; - #binding-cells = <2>; - flavor = "tap-preferred"; - tapping-term-ms = <200>; - quick-tap-ms = <125>; - global-quick-tap; - bindings = <&kp>, <&kp>; -}; -``` - -If you press `&kp A` and then `&gqt LEFT_SHIFT B` **within** 125 ms, then `ab` will be output. Importantly, `b` will be output immediately since it was within the `quick-tap-ms`. This quick-tap behavior will work for any key press, whether it is within a behavior like hold-tap, or a simple `&kp`. This means the `&gqt LEFT_SHIFT B` binding will only have its underlying hold-tap behavior if it is pressed 125 ms **after** a key press. - -Note that the greater the value of `quick-tap-ms` is, the harder it will be to invoke the hold behavior, making this feature less applicable for use-cases like capitalizing letters while typing normally. However, if the hold behavior isn't used during fast typing, then it can be an effective way to mitigate misfires. - -#### `retro-tap` - -If `retro-tap` is enabled, the tap behavior is triggered when releasing the hold-tap key if no other key was pressed in the meantime. - -For example, if you press `&mt LEFT_SHIFT A` and then release it without pressing another key, it will output `a`. - -``` -&mt { - retro-tap; -}; -``` - -To define a behavior to use instead of the tap behavior, include `retro-tap-behavior`, `retro-tap-param1`, and/or `retro-tap-param2` in your hold-tap definition. - -- `retro-tap-behavior` refers to the label string of the desired behavior. See below for a list of these label strings for built-in behaviors. -- `retro-tap-param1` refers to the first thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, LSHFT will be param1. -- `retro-tap-param2` refers to the second thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, A will be param2. - -List of built-in behaviors and their corresponding label strings: - -- &bl - "BCKLGHT" -- &bt - "BLUETOOTH" -- &caps_word - "CAPS_WORD" -- &ext_power - "EXTPOWER" -- &gresc - "GRAVE_ESCAPE" -- &kp - "KEY_PRESS" -- &key_repeat - "KEY_REPEAT" -- &none - "NONE" -- &out - "OUTPUTS" -- &reset - "RESET" -- &bootloader - "BOOTLOAD" -- &rgb_ug - "RGB_UG" -- &sk - "STICKY_KEY" -- &sl - "STICKY_LAYER" -- &to - "TO_LAYER" -- &tog - "TOGGLE_LAYER" -- &trans - "TRANS" - -#### Positional hold-tap and `hold-trigger-key-positions` - -Including `hold-trigger-key-positions` in your hold-tap definition turns on the positional hold-tap feature. With positional hold-tap enabled, if you press any key **NOT** listed in `hold-trigger-key-positions` before `tapping-term-ms` expires, it will produce a tap. - -In all other situations, positional hold-tap will not modify the behavior of your hold-tap. Positional hold-tap is useful when used with home-row modifiers: for example, if you have a home-row modifier key in the left hand, by including only key positions from the right hand in `hold-trigger-key-positions`, you will only get hold behaviors during cross-hand key combinations. - -:::info -Note that `hold-trigger-key-positions` is an array of key position indexes. Key positions are numbered sequentially according to your keymap, starting with 0. So if the first key in your keymap is Q, this key is in position 0. The next key (probably W) will be in position 1, et cetera. -::: - -See the following example, which uses a hold-tap behavior definition, configured with the `hold-preferred` flavor, and with positional hold-tap enabled: - -``` -#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, E_down, E_up, pht_up)` produces `qe`. The normal hold behavior (LEFT_SHIFT) **IS** modified into a tap behavior (Q) by positional hold-tap 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`. -- The sequence `(pht_down, W_down, W_up, pht_up)` produces `W`. The normal hold behavior (LEFT_SHIFT) **is NOT** modified into a tap behavior (Q) by positional hold-tap 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`. -- If the `LEFT_SHIFT / Q key` is held by itself for longer than `tapping-term-ms`, a hold behavior is produced. This is because positional hold-tap only modifies the behavior of a hold-tap if another key is pressed before the `tapping-term-ms` period expires. - -By default, `hold-trigger-key-positions` are evaluated upon the first _key press_ after -the hold-tap. For homerow mods, this is not always ideal, because it prevents combining multiple modifiers unless they are included in `hold-trigger-key-positions`. To overwrite this behavior, one can set `hold-trigger-on-release`. If set to true, the evaluation of `hold-trigger-key-positions` gets delayed until _key release_. This allows combining multiple modifiers when the next key is _held_, while still deciding the hold-tap in favor of a tap when the next key is _tapped_. - -### Example Use-Cases - - - - - -The following are suggested hold-tap configurations that work well with home row mods: - -##### Option 1: cross-hand only modifiers, using `tap-unless-interrupted` and positional hold-tap (`hold-trigger-key-positions`) - -```dtsi title="Homerow Mods: Cross-hand Example" -#include -#include - -/ { - behaviors { - lh_pht: left_hand_positional_hold_tap { - compatible = "zmk,behavior-hold-tap"; - label = "LEFT_POSITIONAL_HOLD_TAP"; - #binding-cells = <2>; - flavor = "tap-unless-interrupted"; - tapping-term-ms = <100>; // <---[[produces tap if held longer than tapping-term-ms]] - quick-tap-ms = <200>; - bindings = <&kp>, <&kp>; - hold-trigger-key-positions = <5 6 7 8 9 10>; // <---[[right-hand keys]] - }; - }; - - keymap { - compatible = "zmk,keymap"; - default_layer { - bindings = < - // position 0 pos 1 pos 2 pos 3 pos 4 pos 5 pos 6 pos 7 pos 8 pos 9 pos 10 - &lh_pht LSFT A &lh_pht LGUI S &lh_pht LALT D &lh_pht LCTL F &kp G &kp H &kp I &kp J &kp K &kp L &kp SEMI - >; - }; - }; -}; -``` - -##### Option 2: `tap-preferred` - -```dtsi title="Homerow Mods: Tap-Preferred Example" -#include -#include - -/ { - behaviors { - hm: homerow_mods { - compatible = "zmk,behavior-hold-tap"; - label = "HOMEROW_MODS"; - #binding-cells = <2>; - tapping-term-ms = <150>; - quick-tap-ms = <0>; - flavor = "tap-preferred"; - bindings = <&kp>, <&kp>; - }; - }; - - keymap { - compatible = "zmk,keymap"; - default_layer { - bindings = < - &hm LCTRL A &hm LGUI S &hm LALT D &hm LSHIFT F - >; - }; - }; -}; -``` - -##### Option 3: `balanced` - -```dtsi title="Homerow Mods: Balanced Example" -#include -#include - -/ { - behaviors { - bhm: balanced_homerow_mods { - compatible = "zmk,behavior-hold-tap"; - label = "HOMEROW_MODS"; - #binding-cells = <2>; - tapping-term-ms = <200>; // <---[[moderate duration]] - quick-tap-ms = <0>; - flavor = "balanced"; - bindings = <&kp>, <&kp>; - }; - }; - - keymap { - compatible = "zmk,keymap"; - default_layer { - bindings = < - &bhm LCTRL A &bhm LGUI S &bhm LALT D &bhm LSHIFT F - >; - }; - }; -}; -``` - - - - - -A popular method of implementing Autoshift in ZMK involves a C-preprocessor macro, commonly defined as `AS(keycode)`. This macro applies the `LSHIFT` modifier to the specified `keycode` when `AS(keycode)` is held, and simply performs a [keypress](key-press.md), `&kp keycode`, when the `AS(keycode)` binding is tapped. This simplifies the use of Autoshift in a keymap, as the complete hold-tap bindings for each desired Autoshift key, as in `&as LS() &as LS() ... &as LS() `, can be quite cumbersome to use when applied to a large portion of the keymap. - -```dtsi title="Hold-Tap Example: Autoshift" -#include -#include - -#define AS(keycode) &as LS(keycode) keycode // Autoshift Macro - -/ { - behaviors { - as: auto_shift { - compatible = "zmk,behavior-hold-tap"; - label = "AUTO_SHIFT"; - #binding-cells = <2>; - tapping_term_ms = <135>; - quick_tap_ms = <0>; - flavor = "tap-preferred"; - bindings = <&kp>, <&kp>; - }; - }; - - keymap { - compatible = "zmk,keymap"; - default_layer { - bindings = < - AS(Q) AS(W) AS(E) AS(R) AS(T) AS(Y) // Autoshift applied for QWERTY keys - >; - }; - }; -}; -``` - - - - - -This hold-tap example implements a [momentary-layer](layers.md/#momentary-layer) when the keybind is held and a [toggle-layer](layers.md/#toggle-layer) when it is tapped. Similar to the Autoshift and Sticky Hold use-cases, a `MO_TOG(layer)` macro is defined such that the `&mo` and `&tog` behaviors can target a single layer. - -```dtsi title="Hold-Tap Example: Momentary layer on Hold, Toggle layer on Tap" -#include -#include - -#define MO_TOG(layer) &mo_tog layer layer // Macro to apply momentary-layer-on-hold/toggle-layer-on-tap to a specific layer - -/ { - behaviors { - mo_tog: behavior_mo_tog { - compatible = "zmk,behavior-hold-tap"; - label = "mo_tog"; - #binding-cells = <2>; - flavor = "hold-preferred"; - tapping-term-ms = <200>; - bindings = <&mo>, <&tog>; - }; - }; - - keymap { - compatible = "zmk,keymap"; - default_layer { - bindings = < - &mo_tog 2 1 // &mo 2 on hold, &tog 1 on tap - MO_TOG(3) // &mo 3 on hold, &tog 3 on 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 the QMK default. diff --git a/docs/docs/keymaps/behaviors/hold-tap.mdx b/docs/docs/keymaps/behaviors/hold-tap.mdx index 755230a29..7da3a155e 100644 --- a/docs/docs/keymaps/behaviors/hold-tap.mdx +++ b/docs/docs/keymaps/behaviors/hold-tap.mdx @@ -460,3 +460,29 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin retro-tap; }; ``` + +To define a behavior to use instead of the tap behavior, include `retro-tap-behavior`, `retro-tap-param1`, and/or `retro-tap-param2` in your hold-tap definition. + +- `retro-tap-behavior` refers to the label string of the desired behavior. See below for a list of these label strings for built-in behaviors. +- `retro-tap-param1` refers to the first thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, LSHFT will be param1. +- `retro-tap-param2` refers to the second thing that comes after the behavior in your keymap. e.g. for `&mt LSHFT A`, A will be param2. + +List of built-in behaviors and their corresponding label strings: + +- &bl - "BCKLGHT" +- &bt - "BLUETOOTH" +- &caps_word - "CAPS_WORD" +- &ext_power - "EXTPOWER" +- &gresc - "GRAVE_ESCAPE" +- &kp - "KEY_PRESS" +- &key_repeat - "KEY_REPEAT" +- &none - "NONE" +- &out - "OUTPUTS" +- &reset - "RESET" +- &bootloader - "BOOTLOAD" +- &rgb_ug - "RGB_UG" +- &sk - "STICKY_KEY" +- &sl - "STICKY_LAYER" +- &to - "TO_LAYER" +- &tog - "TOGGLE_LAYER" +- &trans - "TRANS" From 722189718ffcd588ac93adf521591a5bde1d922f Mon Sep 17 00:00:00 2001 From: Nick Conway Date: Thu, 19 Jun 2025 20:43:50 -0400 Subject: [PATCH 3/3] fix(retro-tap-binding): fix incompatibility between hold-while-undecided and retro-tap --- app/src/behaviors/behavior_hold_tap.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 9a316e1fc..853460a62 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -525,6 +525,10 @@ static int release_binding(struct active_hold_tap *hold_tap) { if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { return release_hold_binding(hold_tap); } else if (hold_tap->status == STATUS_RETRO_TAP) { + if (hold_tap->config->hold_while_undecided) { + release_hold_binding(hold_tap); + } + press_retro_tap_binding(hold_tap); return release_retro_tap_binding(hold_tap); } else {