diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index a2affbf2..bf5035cd 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -31,6 +31,8 @@ properties: - "balanced" - "tap-preferred" - "tap-unless-interrupted" + hold-while-undecided: + type: boolean retro-tap: type: boolean hold-trigger-key-positions: diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 30350ef2..e99bd626 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -59,6 +59,7 @@ struct behavior_hold_tap_config { int quick_tap_ms; bool global_quick_tap; enum flavor flavor; + bool hold_while_undecided; bool retro_tap; bool hold_trigger_on_release; int32_t hold_trigger_key_positions_len; @@ -67,10 +68,12 @@ struct behavior_hold_tap_config { // this data is specific for each hold-tap struct active_hold_tap { + bool first_press; int32_t position; uint32_t param_hold; uint32_t param_tap; int64_t timestamp; + bool hold_released; enum status status; const struct behavior_hold_tap_config *config; struct k_work_delayable work; @@ -219,11 +222,13 @@ static struct active_hold_tap *store_hold_tap(uint32_t position, uint32_t param_ if (active_hold_taps[i].position != ZMK_BHV_HOLD_TAP_POSITION_NOT_USED) { continue; } + active_hold_taps[i].first_press = true; active_hold_taps[i].position = position; active_hold_taps[i].status = STATUS_UNDECIDED; active_hold_taps[i].config = config; active_hold_taps[i].param_hold = param_hold; active_hold_taps[i].param_tap = param_tap; + active_hold_taps[i].hold_released = false; active_hold_taps[i].timestamp = timestamp; active_hold_taps[i].position_of_first_other_key_pressed = -1; return &active_hold_taps[i]; @@ -434,6 +439,28 @@ static void decide_positional_hold(struct active_hold_tap *hold_tap) { hold_tap->status = STATUS_TAP; } +static void release_hold_binding(struct active_hold_tap *hold_tap, + enum decision_moment decision_moment) { + bool keyTap = + !(hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT); + bool holdRelease = + (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) && + decision_moment == HT_KEY_UP; + + if (holdRelease || keyTap) { + LOG_DBG("Releasing hold behavior"); + struct zmk_behavior_binding_event event = { + .position = hold_tap->position, + .timestamp = hold_tap->timestamp, + }; + struct zmk_behavior_binding binding = {0}; + binding.behavior_dev = hold_tap->config->hold_behavior_dev; + binding.param1 = hold_tap->param_hold; + behavior_keymap_binding_released(&binding, event); + hold_tap->hold_released = true; + } +} + static void decide_hold_tap(struct active_hold_tap *hold_tap, enum decision_moment decision_moment) { if (hold_tap->status != STATUS_UNDECIDED) { @@ -473,6 +500,10 @@ static void decide_hold_tap(struct active_hold_tap *hold_tap, status_str(hold_tap->status), flavor_str(hold_tap->config->flavor), decision_moment_str(decision_moment)); undecided_hold_tap = NULL; + + if (hold_tap->config->hold_while_undecided && !hold_tap->hold_released) { + release_hold_binding(hold_tap, decision_moment); + } press_binding(hold_tap); release_captured_events(); } @@ -530,6 +561,12 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, if (is_quick_tap(hold_tap)) { decide_hold_tap(hold_tap, HT_QUICK_TAP); + } else if (cfg->hold_while_undecided) { + struct zmk_behavior_binding hold_binding = {0}; + hold_binding.behavior_dev = cfg->hold_behavior_dev; + hold_binding.param1 = binding->param1; + LOG_DBG("%d hold behavior pressed while undecided", event.position); + behavior_keymap_binding_pressed(&hold_binding, event); } // if this behavior was queued we have to adjust the timer to only @@ -556,6 +593,9 @@ static int on_hold_tap_binding_released(struct zmk_behavior_binding *binding, } decide_hold_tap(hold_tap, HT_KEY_UP); + if (hold_tap->config->hold_while_undecided && !hold_tap->hold_released) { + release_hold_binding(hold_tap, HT_KEY_UP); + } decide_retro_tap(hold_tap); release_binding(hold_tap); @@ -652,6 +692,11 @@ static int keycode_state_changed_listener(const zmk_event_t *eh) { return ZMK_EV_EVENT_BUBBLE; } + if (undecided_hold_tap->first_press && undecided_hold_tap->config->hold_while_undecided) { + undecided_hold_tap->first_press = false; + return ZMK_EV_EVENT_BUBBLE; + } + // only key-up events will bubble through position_state_changed_listener // if a undecided_hold_tap is active. LOG_DBG("%d capturing 0x%02X %s event", undecided_hold_tap->position, ev->keycode, @@ -705,6 +750,7 @@ static int behavior_hold_tap_init(const struct device *dev) { .quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \ .global_quick_tap = DT_INST_PROP(n, global_quick_tap), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ + .hold_while_undecided = DT_INST_PROP(n, hold_while_undecided), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ .hold_trigger_key_positions = DT_INST_PROP(n, hold_trigger_key_positions), \ diff --git a/app/tests/hold-tap/balanced/behavior_keymap_hwu.dtsi b/app/tests/hold-tap/balanced/behavior_keymap_hwu.dtsi new file mode 100644 index 00000000..6491c431 --- /dev/null +++ b/app/tests/hold-tap/balanced/behavior_keymap_hwu.dtsi @@ -0,0 +1,29 @@ +#include +#include +#include + +/ { + behaviors { + ht_bal: behavior_hold_tap_balanced { + compatible = "zmk,behavior-hold-tap"; + label = "HOLD_TAP_BALANCED"; + #binding-cells = <2>; + flavor = "balanced"; + tapping-term-ms = <300>; + quick-tap-ms = <200>; + bindings = <&kp>, <&kp>; + hold-while-undecided; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &ht_bal LEFT_SHIFT A &ht_bal LEFT_CONTROL B + &kp D &kp RIGHT_CONTROL>; + }; + }; +}; diff --git a/app/tests/hold-tap/balanced/hold_while_undecided/events.patterns b/app/tests/hold-tap/balanced/hold_while_undecided/events.patterns new file mode 100644 index 00000000..fdf2b15c --- /dev/null +++ b/app/tests/hold-tap/balanced/hold_while_undecided/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/hold_while_undecided/keycode_events.snapshot b/app/tests/hold-tap/balanced/hold_while_undecided/keycode_events.snapshot new file mode 100644 index 00000000..6ec0e354 --- /dev/null +++ b/app/tests/hold-tap/balanced/hold_while_undecided/keycode_events.snapshot @@ -0,0 +1,8 @@ +ht_binding_pressed: 0 new undecided hold_tap +ht_binding_pressed: 0 hold behavior pressed while undecided +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_decide: 0 decided tap (balanced decision moment key-up) +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap diff --git a/app/tests/hold-tap/balanced/hold_while_undecided/native_posix_64.keymap b/app/tests/hold-tap/balanced/hold_while_undecided/native_posix_64.keymap new file mode 100644 index 00000000..90dedde0 --- /dev/null +++ b/app/tests/hold-tap/balanced/hold_while_undecided/native_posix_64.keymap @@ -0,0 +1,11 @@ +#include +#include +#include +#include "../behavior_keymap_hwu.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/docs/docs/behaviors/hold-tap.md b/docs/docs/behaviors/hold-tap.md index f0096606..9fc29af6 100644 --- a/docs/docs/behaviors/hold-tap.md +++ b/docs/docs/behaviors/hold-tap.md @@ -84,6 +84,14 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin }; ``` +#### `hold-while-undecided` + +If enabled, the hold behavior will immediately be held, and will release before the tap behavior is sent. With modifiers (excluding the windows key) this will not affect typing, and is useful for mod + clicking. + +:::note Alt behavior +In some applications, pressing the alt key by itself will have its own behavior like activate a menu. +::: + #### 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.