diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index 5f74e9ad787..56f0cc27213 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -13,6 +13,9 @@ properties: required: true tapping_term_ms: type: int + quick_tap_ms: + type: int + default: -1 flavor: type: string required: false diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index fcb4c5bd8fc..6d20942214a 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -42,6 +42,7 @@ struct behavior_hold_tap_behaviors { struct behavior_hold_tap_config { int tapping_term_ms; + int quick_tap_ms; struct behavior_hold_tap_behaviors *behaviors; enum flavor flavor; }; @@ -70,6 +71,24 @@ struct active_hold_tap active_hold_taps[ZMK_BHV_HOLD_TAP_MAX_HELD] = {}; // We capture most position_state_changed events and some modifiers_state_changed events. const zmk_event_t *captured_events[ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS] = {}; +// Keep track of which key was tapped most recently for 'quick_tap_ms' +struct last_tapped { + int32_t position; + int64_t tap_deadline; +}; + +struct last_tapped last_tapped; + +static void store_last_tapped(struct active_hold_tap *hold_tap) { + last_tapped.position = hold_tap->position; + last_tapped.tap_deadline = hold_tap->timestamp + hold_tap->config->quick_tap_ms; +} + +static bool is_quick_tap(struct active_hold_tap *hold_tap) { + return last_tapped.position == hold_tap->position && + last_tapped.tap_deadline > hold_tap->timestamp; +} + static int capture_event(const zmk_event_t *event) { for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) { if (captured_events[i] == NULL) { @@ -194,6 +213,7 @@ enum decision_moment { HT_OTHER_KEY_DOWN = 1, HT_OTHER_KEY_UP = 2, HT_TIMER_EVENT = 3, + HT_QUICK_TAP = 4, }; static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_moment event) { @@ -207,6 +227,10 @@ static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_mome hold_tap->is_hold = 1; hold_tap->is_decided = true; break; + case HT_QUICK_TAP: + hold_tap->is_hold = 0; + hold_tap->is_decided = true; + break; default: return; } @@ -222,6 +246,10 @@ static void decide_tap_preferred(struct active_hold_tap *hold_tap, enum decision hold_tap->is_hold = 1; hold_tap->is_decided = true; break; + case HT_QUICK_TAP: + hold_tap->is_hold = 0; + hold_tap->is_decided = true; + break; default: return; } @@ -238,6 +266,10 @@ static void decide_hold_preferred(struct active_hold_tap *hold_tap, enum decisio hold_tap->is_hold = 1; hold_tap->is_decided = true; break; + case HT_QUICK_TAP: + hold_tap->is_hold = 0; + hold_tap->is_decided = true; + break; default: return; } @@ -296,6 +328,7 @@ static void decide_hold_tap(struct active_hold_tap *hold_tap, enum decision_mome binding.behavior_dev = hold_tap->config->behaviors->tap.behavior_dev; binding.param1 = hold_tap->param_tap; binding.param2 = 0; + store_last_tapped(hold_tap); } behavior_keymap_binding_pressed(&binding, event); release_captured_events(); @@ -323,6 +356,10 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, LOG_DBG("%d new undecided hold_tap", event.position); undecided_hold_tap = hold_tap; + if (is_quick_tap(hold_tap)) { + decide_hold_tap(hold_tap, HT_QUICK_TAP); + } + // if this behavior was queued we have to adjust the timer to only // wait for the remaining time. int32_t tapping_term_ms_left = (hold_tap->timestamp + cfg->tapping_term_ms) - k_uptime_get(); @@ -505,6 +542,7 @@ static struct behavior_hold_tap_data behavior_hold_tap_data; static struct behavior_hold_tap_config behavior_hold_tap_config_##n = { \ .behaviors = &behavior_hold_tap_behaviors_##n, \ .tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \ + .quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ }; \ DEVICE_AND_API_INIT(behavior_hold_tap_##n, DT_INST_LABEL(n), behavior_hold_tap_init, \ diff --git a/app/tests/hold-tap/balanced/5-quick-tap/events.patterns b/app/tests/hold-tap/balanced/5-quick-tap/events.patterns new file mode 100644 index 00000000000..fdf2b15cf25 --- /dev/null +++ b/app/tests/hold-tap/balanced/5-quick-tap/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*mo_keymap_binding/mo/p +s/.*on_hold_tap_binding/ht_binding/p +s/.*decide_hold_tap/ht_decide/p \ No newline at end of file diff --git a/app/tests/hold-tap/balanced/5-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/balanced/5-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..a1b18d1c806 --- /dev/null +++ b/app/tests/hold-tap/balanced/5-quick-tap/keycode_events.snapshot @@ -0,0 +1,10 @@ +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (balanced event 0) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (balanced event 4) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap diff --git a/app/tests/hold-tap/balanced/5-quick-tap/native_posix.keymap b/app/tests/hold-tap/balanced/5-quick-tap/native_posix.keymap new file mode 100644 index 00000000000..8f90ffadb4e --- /dev/null +++ b/app/tests/hold-tap/balanced/5-quick-tap/native_posix.keymap @@ -0,0 +1,14 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/hold-tap/balanced/behavior_keymap.dtsi b/app/tests/hold-tap/balanced/behavior_keymap.dtsi index 34a4d458482..972b606fd3a 100644 --- a/app/tests/hold-tap/balanced/behavior_keymap.dtsi +++ b/app/tests/hold-tap/balanced/behavior_keymap.dtsi @@ -10,6 +10,7 @@ #binding-cells = <2>; flavor = "balanced"; tapping_term_ms = <300>; + quick_tap_ms = <200>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/tests/hold-tap/hold-preferred/5-quick-tap/events.patterns b/app/tests/hold-tap/hold-preferred/5-quick-tap/events.patterns new file mode 100644 index 00000000000..fdf2b15cf25 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/5-quick-tap/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*mo_keymap_binding/mo/p +s/.*on_hold_tap_binding/ht_binding/p +s/.*decide_hold_tap/ht_decide/p \ No newline at end of file diff --git a/app/tests/hold-tap/hold-preferred/5-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/5-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..c3caf8704c3 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/5-quick-tap/keycode_events.snapshot @@ -0,0 +1,10 @@ +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (hold-preferred event 0) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (hold-preferred event 4) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap diff --git a/app/tests/hold-tap/hold-preferred/5-quick-tap/native_posix.keymap b/app/tests/hold-tap/hold-preferred/5-quick-tap/native_posix.keymap new file mode 100644 index 00000000000..8f90ffadb4e --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/5-quick-tap/native_posix.keymap @@ -0,0 +1,14 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/hold-tap/hold-preferred/behavior_keymap.dtsi b/app/tests/hold-tap/hold-preferred/behavior_keymap.dtsi index e6143195230..2b35f890f74 100644 --- a/app/tests/hold-tap/hold-preferred/behavior_keymap.dtsi +++ b/app/tests/hold-tap/hold-preferred/behavior_keymap.dtsi @@ -12,6 +12,7 @@ #binding-cells = <2>; flavor = "hold-preferred"; tapping_term_ms = <300>; + quick_tap_ms = <200>; bindings = <&kp>, <&kp>; }; }; diff --git a/app/tests/hold-tap/tap-preferred/5-quick-tap/events.patterns b/app/tests/hold-tap/tap-preferred/5-quick-tap/events.patterns new file mode 100644 index 00000000000..fdf2b15cf25 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/5-quick-tap/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*mo_keymap_binding/mo/p +s/.*on_hold_tap_binding/ht_binding/p +s/.*decide_hold_tap/ht_decide/p \ No newline at end of file diff --git a/app/tests/hold-tap/tap-preferred/5-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/5-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..e89ccf343be --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/5-quick-tap/keycode_events.snapshot @@ -0,0 +1,10 @@ +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-preferred event 0) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-preferred event 4) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap diff --git a/app/tests/hold-tap/tap-preferred/5-quick-tap/native_posix.keymap b/app/tests/hold-tap/tap-preferred/5-quick-tap/native_posix.keymap new file mode 100644 index 00000000000..8f90ffadb4e --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/5-quick-tap/native_posix.keymap @@ -0,0 +1,14 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/hold-tap/tap-preferred/behavior_keymap.dtsi b/app/tests/hold-tap/tap-preferred/behavior_keymap.dtsi index e6d33c0b693..80d6b0afc92 100644 --- a/app/tests/hold-tap/tap-preferred/behavior_keymap.dtsi +++ b/app/tests/hold-tap/tap-preferred/behavior_keymap.dtsi @@ -10,6 +10,7 @@ #binding-cells = <2>; flavor = "tap-preferred"; tapping_term_ms = <300>; + quick_tap_ms = <200>; bindings = <&kp>, <&kp>; }; }; diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index 0cf48884be1..c148fa37758 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -11,7 +11,7 @@ Simply put, the hold-tap key will output the 'hold' behavior if it's held for a ### Hold-Tap -The `tapping_term_ms` parameter decides between a 'tap' and a 'hold'. +The graph below shows how the hold-tap decides between a 'tap' and a 'hold'. ![Simple behavior](../assets/hold-tap/case1_2.png) @@ -37,6 +37,18 @@ For basic usage, please see [mod-tap](./mod-tap.md) and [layer-tap](./layers.md) ### 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). + +In QMK, unlike ZMK, this functionality is enabled by default, and you turn it off using `TAPPING_FORCE_HOLD`. + +#### Home row mods + This example configures a hold-tap that works well for homerow mods: ``` @@ -50,6 +62,7 @@ This example configures a hold-tap that works well for homerow mods: label = "HOMEROW_MODS"; #binding-cells = <2>; tapping_term_ms = <150>; + quick_tap_ms = <0>; flavor = "tap-preferred"; bindings = <&kp>, <&kp>; };