diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index 76f14d12d4e..e9d30924ba8 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -28,6 +28,10 @@ properties: require-prior-idle-ms: type: int default: -1 + exclude-prior-idle-key-positions: + type: array + required: false + default: [] flavor: type: string required: false diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 3df3bc86436..c946e04e6fe 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -58,16 +58,19 @@ struct behavior_hold_tap_config { char *tap_behavior_dev; int quick_tap_ms; int require_prior_idle_ms; + int32_t exclude_prior_idle_key_positions_len; + int32_t *exclude_prior_idle_key_positions; enum flavor flavor; bool hold_while_undecided; bool hold_while_undecided_linger; bool retro_tap; bool hold_trigger_on_release; int32_t hold_trigger_key_positions_len; - int32_t hold_trigger_key_positions[]; + int32_t *hold_trigger_key_positions; }; struct behavior_hold_tap_data { + int64_t last_prior_idle_key_timestamp; #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) struct behavior_parameter_metadata_set set; #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) @@ -84,8 +87,10 @@ struct active_hold_tap { int64_t timestamp; enum status status; const struct behavior_hold_tap_config *config; + struct behavior_hold_tap_data *data; struct k_work_delayable work; bool work_is_cancelled; + bool quick_tap_deferred; // initialized to -1, which is to be interpreted as "no other key has been pressed yet" int32_t position_of_first_other_key_pressed; @@ -129,6 +134,11 @@ struct last_tapped { // int64 min since it will overflow if -1 is added struct last_tapped last_tapped = {INT32_MIN, INT32_MIN}; +// Tracks the position of the most recently bubbled keydown event. Used by the +// keycode listener to call update_prior_idle_key_timestamps with the correct +// position (keycode events don't carry position information). +int32_t last_keydown_position = INT32_MIN; + static void store_last_tapped(int64_t timestamp) { if (timestamp > last_tapped.timestamp) { last_tapped.position = INT32_MIN; @@ -141,13 +151,49 @@ static void store_last_hold_tapped(struct active_hold_tap *hold_tap) { last_tapped.timestamp = hold_tap->timestamp; } +static bool is_exclude_prior_idle_key_position(const struct behavior_hold_tap_config *config, + int32_t position) { + for (int i = 0; i < config->exclude_prior_idle_key_positions_len; i++) { + if (config->exclude_prior_idle_key_positions[i] == position) { + return true; + } + } + return false; +} + +// Registry of all hold-tap device instances, populated during init, so the +// position listener can update per-instance last_prior_idle_key_timestamp. +#define HT_MAX_DEVICES DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) +static const struct device *ht_devices[HT_MAX_DEVICES]; +static int ht_num_devices = 0; + +static void update_prior_idle_key_timestamps(int32_t position, int64_t timestamp) { + for (int i = 0; i < ht_num_devices; i++) { + const struct behavior_hold_tap_config *cfg = ht_devices[i]->config; + struct behavior_hold_tap_data *data = ht_devices[i]->data; + if (cfg->exclude_prior_idle_key_positions_len > 0) { + if (is_exclude_prior_idle_key_position(cfg, position)) { + data->last_prior_idle_key_timestamp = INT32_MIN; + } else { + data->last_prior_idle_key_timestamp = timestamp; + } + } + } +} + static bool is_quick_tap(struct active_hold_tap *hold_tap) { - if ((last_tapped.timestamp + hold_tap->config->require_prior_idle_ms) > hold_tap->timestamp) { + if (hold_tap->config->exclude_prior_idle_key_positions_len > 0) { + if ((hold_tap->data->last_prior_idle_key_timestamp + + hold_tap->config->require_prior_idle_ms) > hold_tap->timestamp) { + return true; + } + } else if ((last_tapped.timestamp + hold_tap->config->require_prior_idle_ms) > + hold_tap->timestamp) { return true; - } else { - return (last_tapped.position == hold_tap->position) && - (last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp; } + + return (last_tapped.position == hold_tap->position) && + (last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp; } static int capture_event(struct captured_event *data) { @@ -254,7 +300,8 @@ static struct active_hold_tap *find_hold_tap(uint32_t position) { static struct active_hold_tap *store_hold_tap(struct zmk_behavior_binding_event *event, uint32_t param_hold, uint32_t param_tap, - const struct behavior_hold_tap_config *config) { + const struct behavior_hold_tap_config *config, + struct behavior_hold_tap_data *data) { for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_HELD; i++) { if (active_hold_taps[i].position != ZMK_BHV_HOLD_TAP_POSITION_NOT_USED) { continue; @@ -265,9 +312,11 @@ static struct active_hold_tap *store_hold_tap(struct zmk_behavior_binding_event #endif active_hold_taps[i].status = STATUS_UNDECIDED; active_hold_taps[i].config = config; + active_hold_taps[i].data = data; active_hold_taps[i].param_hold = param_hold; active_hold_taps[i].param_tap = param_tap; active_hold_taps[i].timestamp = event->timestamp; + active_hold_taps[i].quick_tap_deferred = false; active_hold_taps[i].position_of_first_other_key_pressed = -1; return &active_hold_taps[i]; } @@ -607,6 +656,7 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event) { const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); const struct behavior_hold_tap_config *cfg = dev->config; + struct behavior_hold_tap_data *data = dev->data; if (undecided_hold_tap != NULL) { LOG_DBG("ERROR another hold-tap behavior is undecided."); @@ -615,7 +665,7 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, } struct active_hold_tap *hold_tap = - store_hold_tap(&event, binding->param1, binding->param2, cfg); + store_hold_tap(&event, binding->param1, binding->param2, cfg, data); if (hold_tap == NULL) { LOG_ERR("unable to store hold-tap info, did you press more than %d hold-taps?", @@ -627,7 +677,11 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, undecided_hold_tap = hold_tap; if (is_quick_tap(hold_tap)) { - decide_hold_tap(hold_tap, HT_QUICK_TAP); + if (hold_tap->config->exclude_prior_idle_key_positions_len > 0) { + hold_tap->quick_tap_deferred = true; + } else { + decide_hold_tap(hold_tap, HT_QUICK_TAP); + } } decide_hold_tap(hold_tap, HT_KEY_DOWN); @@ -651,6 +705,12 @@ static int on_hold_tap_binding_released(struct zmk_behavior_binding *binding, // If these events were queued, the timer event may be queued too late or not at all. // We insert a timer event before the TH_KEY_UP event to verify. int work_cancel_result = k_work_cancel_delayable(&hold_tap->work); + + if (hold_tap->quick_tap_deferred) { + hold_tap->quick_tap_deferred = false; + decide_hold_tap(hold_tap, HT_QUICK_TAP); + } + if (event.timestamp > (hold_tap->timestamp + hold_tap->config->tapping_term_ms)) { decide_hold_tap(hold_tap, HT_TIMER_EVENT); } @@ -730,6 +790,9 @@ static int position_state_changed_listener(const zmk_event_t *eh) { update_hold_status_for_retro_tap(ev->position); if (undecided_hold_tap == NULL) { + if (ev->state) { + last_keydown_position = ev->position; + } LOG_DBG("%d bubble (no undecided hold_tap active)", ev->position); return ZMK_EV_EVENT_BUBBLE; } @@ -754,6 +817,20 @@ static int position_state_changed_listener(const zmk_event_t *eh) { } } + // Resolve deferred quick-tap based on the combo key pressed. + // If the combo key is in exclude_prior_idle_key_positions, skip quick-tap entirely + // and let normal flavor logic handle the decision (as if require-prior-idle-ms was not set). + // If the combo key is NOT in the list, fire the deferred quick-tap now. + if (ev->state && undecided_hold_tap->quick_tap_deferred) { + undecided_hold_tap->quick_tap_deferred = false; + if (!is_exclude_prior_idle_key_position(undecided_hold_tap->config, ev->position)) { + decide_hold_tap(undecided_hold_tap, HT_QUICK_TAP); + if (undecided_hold_tap == NULL) { + return ZMK_EV_EVENT_BUBBLE; + } + } + } + // If these events were queued, the timer event may be queued too late or not at all. // We make a timer decision before the other key events are handled if the timer would // have run out. @@ -791,6 +868,7 @@ static int keycode_state_changed_listener(const zmk_event_t *eh) { if (ev->state && !is_mod(ev->usage_page, ev->keycode)) { store_last_tapped(ev->timestamp); + update_prior_idle_key_timestamps(last_keydown_position, ev->timestamp); } if (undecided_hold_tap == NULL) { @@ -840,7 +918,12 @@ void behavior_hold_tap_timer_work_handler(struct k_work *item) { if (hold_tap->work_is_cancelled) { clear_hold_tap(hold_tap); } else { - decide_hold_tap(hold_tap, HT_TIMER_EVENT); + if (hold_tap->quick_tap_deferred) { + hold_tap->quick_tap_deferred = false; + decide_hold_tap(hold_tap, HT_QUICK_TAP); + } else { + decide_hold_tap(hold_tap, HT_TIMER_EVENT); + } } } @@ -854,10 +937,26 @@ static int behavior_hold_tap_init(const struct device *dev) { } } init_first_run = false; + + struct behavior_hold_tap_data *data = dev->data; + data->last_prior_idle_key_timestamp = INT32_MIN; + + if (ht_num_devices < HT_MAX_DEVICES) { + ht_devices[ht_num_devices++] = dev; + } + return 0; } #define KP_INST(n) \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, hold_trigger_key_positions), \ + (static const int32_t hold_trigger_key_positions_##n[] = \ + DT_INST_PROP(n, hold_trigger_key_positions);), \ + ()) \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, exclude_prior_idle_key_positions), \ + (static const int32_t exclude_prior_idle_key_positions_##n[] = \ + DT_INST_PROP(n, exclude_prior_idle_key_positions);), \ + ()) \ static const struct behavior_hold_tap_config behavior_hold_tap_config_##n = { \ .tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \ .hold_behavior_dev = DEVICE_DT_NAME(DT_INST_PHANDLE_BY_IDX(n, bindings, 0)), \ @@ -866,12 +965,19 @@ static int behavior_hold_tap_init(const struct device *dev) { .require_prior_idle_ms = DT_INST_PROP(n, global_quick_tap) \ ? DT_INST_PROP(n, quick_tap_ms) \ : DT_INST_PROP(n, require_prior_idle_ms), \ + .exclude_prior_idle_key_positions = \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, exclude_prior_idle_key_positions), \ + (exclude_prior_idle_key_positions_##n), (NULL)), \ + .exclude_prior_idle_key_positions_len = \ + DT_INST_PROP_LEN(n, exclude_prior_idle_key_positions), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ .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), \ .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 = \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, hold_trigger_key_positions), \ + (hold_trigger_key_positions_##n), (NULL)), \ .hold_trigger_key_positions_len = DT_INST_PROP_LEN(n, hold_trigger_key_positions), \ }; \ static struct behavior_hold_tap_data behavior_hold_tap_data_##n = {}; \ diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/events.patterns b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot new file mode 100644 index 00000000000..10661f9c68d --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (balanced decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (balanced decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap new file mode 100644 index 00000000000..ed38220a118 --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press exclude key (pos 2, in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer cancelled by exclude key, ht_a goes through normal hold decision */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press non-exclude key (pos 3, NOT in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer reset by non-exclude key, ht_a resolves as quick-tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot new file mode 100644 index 00000000000..5261fcd0216 --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (balanced decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 1 new undecided hold_tap +ht_decide: 1 decided tap (balanced decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 1 cleaning up hold-tap diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap new file mode 100644 index 00000000000..6d5466e72c3 --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press pos 2 (D), which is in ht_a's exclude list but NOT in ht_b's */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + /* quickly press ht_a (pos 0): pos 2 cancels idle for ht_a, expect hold decision */ + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press pos 2 (D) again, then quickly press ht_b */ + /* pos 2 is NOT in ht_b's exclude list, expect quick-tap */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(0,1,400) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..8379feecefc --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot @@ -0,0 +1,18 @@ +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-interrupt (balanced decision moment other-key-up) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (balanced decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap new file mode 100644 index 00000000000..39889868b0b --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred because exclude list is configured */ + /* then press exclude key (pos 2, in ht_a exclude list) in combo */ + /* expect: quick-tap bypassed, normal balanced decision: hold */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,0,10) + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred */ + /* then press non-exclude key (pos 3) in combo */ + /* expect: deferred quick-tap fires, ht_a resolves as tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,1,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi new file mode 100644 index 00000000000..22ccc16799b --- /dev/null +++ b/app/tests/hold-tap/balanced/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +/ { + behaviors { + ht_a: behavior_ht_a { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "balanced"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <2>; + bindings = <&kp>, <&kp>; + }; + ht_b: behavior_ht_b { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "balanced"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <3>; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &ht_a LEFT_SHIFT F &ht_b LEFT_CONTROL C + &kp D &kp E>; + }; + }; +}; diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot new file mode 100644 index 00000000000..169f51f9f62 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (hold-preferred decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (hold-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap new file mode 100644 index 00000000000..ed38220a118 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press exclude key (pos 2, in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer cancelled by exclude key, ht_a goes through normal hold decision */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press non-exclude key (pos 3, NOT in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer reset by non-exclude key, ht_a resolves as quick-tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot new file mode 100644 index 00000000000..acd1ff08c35 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (hold-preferred decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 1 new undecided hold_tap +ht_decide: 1 decided tap (hold-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 1 cleaning up hold-tap diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap new file mode 100644 index 00000000000..6d5466e72c3 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press pos 2 (D), which is in ht_a's exclude list but NOT in ht_b's */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + /* quickly press ht_a (pos 0): pos 2 cancels idle for ht_a, expect hold decision */ + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press pos 2 (D) again, then quickly press ht_b */ + /* pos 2 is NOT in ht_b's exclude list, expect quick-tap */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(0,1,400) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..c585877326f --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot @@ -0,0 +1,18 @@ +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-interrupt (hold-preferred decision moment other-key-down) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (hold-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap new file mode 100644 index 00000000000..682b9473e97 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred because exclude list is configured */ + /* then press exclude key (pos 2, in ht_a exclude list) in combo */ + /* expect: quick-tap bypassed, normal hold-preferred decision: hold on other-key-down */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,0,10) + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred */ + /* then press non-exclude key (pos 3) in combo */ + /* expect: deferred quick-tap fires, ht_a resolves as tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,1,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi new file mode 100644 index 00000000000..042fe3787e0 --- /dev/null +++ b/app/tests/hold-tap/hold-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +/ { + behaviors { + ht_a: behavior_ht_a { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "hold-preferred"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <2>; + bindings = <&kp>, <&kp>; + }; + ht_b: behavior_ht_b { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "hold-preferred"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <3>; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &ht_a LEFT_SHIFT F &ht_b LEFT_CONTROL C + &kp D &kp E>; + }; + }; +}; diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot new file mode 100644 index 00000000000..78bc767b314 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (tap-preferred decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap new file mode 100644 index 00000000000..ed38220a118 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press exclude key (pos 2, in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer cancelled by exclude key, ht_a goes through normal hold decision */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press non-exclude key (pos 3, NOT in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer reset by non-exclude key, ht_a resolves as quick-tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot new file mode 100644 index 00000000000..195999136c1 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (tap-preferred decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 1 new undecided hold_tap +ht_decide: 1 decided tap (tap-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 1 cleaning up hold-tap diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap new file mode 100644 index 00000000000..6d5466e72c3 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press pos 2 (D), which is in ht_a's exclude list but NOT in ht_b's */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + /* quickly press ht_a (pos 0): pos 2 cancels idle for ht_a, expect hold decision */ + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press pos 2 (D) again, then quickly press ht_b */ + /* pos 2 is NOT in ht_b's exclude list, expect quick-tap */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(0,1,400) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..89f2ac73236 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot @@ -0,0 +1,18 @@ +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-timer (tap-preferred decision moment timer) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-preferred decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap new file mode 100644 index 00000000000..4d3cc28a917 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred because exclude list is configured */ + /* then press exclude key (pos 2, in ht_a exclude list) in combo */ + /* expect: quick-tap bypassed, normal tap-preferred decision: hold on timer */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,400) + ZMK_MOCK_RELEASE(0,0,10) + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred */ + /* then press non-exclude key (pos 3) in combo */ + /* expect: deferred quick-tap fires, ht_a resolves as tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,1,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi new file mode 100644 index 00000000000..714db2371a6 --- /dev/null +++ b/app/tests/hold-tap/tap-preferred/9-exclude-prior-idle-key-positions/behavior_keymap.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +/ { + behaviors { + ht_a: behavior_ht_a { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-preferred"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <2>; + bindings = <&kp>, <&kp>; + }; + ht_b: behavior_ht_b { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-preferred"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <3>; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &ht_a LEFT_SHIFT F &ht_b LEFT_CONTROL C + &kp D &kp E>; + }; + }; +}; diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/events.patterns b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot new file mode 100644 index 00000000000..9b9485bce95 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-unless-interrupted decision moment timer) +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 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-unless-interrupted decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap new file mode 100644 index 00000000000..ed38220a118 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/1-basic/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press exclude key (pos 2, in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer cancelled by exclude key, ht_a goes through normal hold decision */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press non-exclude key (pos 3, NOT in ht_a exclude list), then quickly press ht_a */ + /* expect: idle timer reset by non-exclude key, ht_a resolves as quick-tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/events.patterns b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot new file mode 100644 index 00000000000..0c58aa87249 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/keycode_events.snapshot @@ -0,0 +1,14 @@ +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-unless-interrupted decision moment timer) +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 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 1 new undecided hold_tap +ht_decide: 1 decided tap (tap-unless-interrupted decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 1 cleaning up hold-tap diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap new file mode 100644 index 00000000000..6d5466e72c3 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/2-independent-configs/native_posix_64.keymap @@ -0,0 +1,21 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press pos 2 (D), which is in ht_a's exclude list but NOT in ht_b's */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + /* quickly press ht_a (pos 0): pos 2 cancels idle for ht_a, expect hold decision */ + ZMK_MOCK_PRESS(0,0,400) + ZMK_MOCK_RELEASE(0,0,400) + /* press pos 2 (D) again, then quickly press ht_b */ + /* pos 2 is NOT in ht_b's exclude list, expect quick-tap */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(0,1,400) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns new file mode 100644 index 00000000000..41e5849ecae --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/events.patterns @@ -0,0 +1,6 @@ +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 +s/.*update_hold_status_for_retro_tap/update_hold_status_for_retro_tap/p +s/.*decide_retro_tap/decide_retro_tap/p diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot new file mode 100644 index 00000000000..b54cec789a0 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/keycode_events.snapshot @@ -0,0 +1,18 @@ +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided hold-interrupt (tap-unless-interrupted decision moment other-key-down) +kp_pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_released: 0 cleaning up hold-tap +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +ht_binding_pressed: 0 new undecided hold_tap +ht_decide: 0 decided tap (tap-unless-interrupted decision moment quick-tap) +kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 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-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap new file mode 100644 index 00000000000..32cc44b4280 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/3-deferred-quick-tap/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred because exclude list is configured */ + /* then press exclude key (pos 2, in ht_a exclude list) in combo */ + /* expect: quick-tap bypassed, normal tap-unless-interrupted decision: hold on other-key-down */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(0,0,10) + /* press non-exclude key (pos 3), then quickly press ht_a (pos 0) */ + /* quick-tap deferred */ + /* then press non-exclude key (pos 3) in combo */ + /* expect: deferred quick-tap fires, ht_a resolves as tap */ + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,1,400) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/behavior_keymap.dtsi b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/behavior_keymap.dtsi new file mode 100644 index 00000000000..d9221543125 --- /dev/null +++ b/app/tests/hold-tap/tap-unless-interrupted/7-exclude-prior-idle-key-positions/behavior_keymap.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +/ { + behaviors { + ht_a: behavior_ht_a { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-unless-interrupted"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <2>; + bindings = <&kp>, <&kp>; + }; + ht_b: behavior_ht_b { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-unless-interrupted"; + tapping-term-ms = <300>; + require-prior-idle-ms = <100>; + exclude-prior-idle-key-positions = <3>; + bindings = <&kp>, <&kp>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &ht_a LEFT_SHIFT F &ht_b LEFT_CONTROL C + &kp D &kp E>; + }; + }; +}; diff --git a/docs/docs/config/behaviors.md b/docs/docs/config/behaviors.md index ce53ee99296..642a317bf9f 100644 --- a/docs/docs/config/behaviors.md +++ b/docs/docs/config/behaviors.md @@ -72,19 +72,20 @@ Definition file: [zmk/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml](htt Applies to: `compatible = "zmk,behavior-hold-tap"` -| Property | Type | Description | Default | -| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- | ------------------ | -| `#binding-cells` | int | Must be `<2>` | | -| `bindings` | phandles | A list of two behaviors (without parameters): one for hold and one for tap | | -| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | -| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | -| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | -| `require-prior-idle-ms` | int | Triggers a tap immediately if any non-modifier key was pressed within `require-prior-idle-ms` of the hold-tap | -1 (disabled) | -| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | -| `hold-while-undecided` | bool | Triggers the hold behavior immediately on press and releases before a tap | false | -| `hold-while-undecided-linger` | bool | Continues to hold the hold behavior until after the tap is released | false | -| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap | | -| `hold-trigger-on-release` | bool | If set, delays the evaluation of `hold-trigger-key-positions` until key release | false | +| Property | Type | Description | Default | +| --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- | ------------------ | +| `#binding-cells` | int | Must be `<2>` | | +| `bindings` | phandles | A list of two behaviors (without parameters): one for hold and one for tap | | +| `flavor` | string | Adjusts how the behavior chooses between hold and tap | `"hold-preferred"` | +| `tapping-term-ms` | int | How long in milliseconds the key must be held to trigger a hold | | +| `quick-tap-ms` | int | Tap twice within this period (in milliseconds) to trigger a tap, even when held | -1 (disabled) | +| `require-prior-idle-ms` | int | Triggers a tap immediately if any non-modifier key was pressed within `require-prior-idle-ms` of the hold-tap | -1 (disabled) | +| `exclude-prior-idle-key-positions` | array | Key positions excluded from the `require-prior-idle-ms` idle timer: cancel the timer when pressed before the hold-tap, and bypass quick-tap when pressed after | | +| `retro-tap` | bool | Triggers the tap behavior on release if no other key was pressed during a hold | false | +| `hold-while-undecided` | bool | Triggers the hold behavior immediately on press and releases before a tap | false | +| `hold-while-undecided-linger` | bool | Continues to hold the hold behavior until after the tap is released | false | +| `hold-trigger-key-positions` | array | If set, pressing the hold-tap and then any key position _not_ in the list triggers a tap | | +| `hold-trigger-on-release` | bool | If set, delays the evaluation of `hold-trigger-key-positions` until key release | false | This behavior forwards the first parameter it receives to the parameter of the first behavior specified in `bindings`, and second parameter to the parameter of the second behavior. diff --git a/docs/docs/keymaps/behaviors/hold-tap.mdx b/docs/docs/keymaps/behaviors/hold-tap.mdx index 8ba945d433e..cca3c62c6e8 100644 --- a/docs/docs/keymaps/behaviors/hold-tap.mdx +++ b/docs/docs/keymaps/behaviors/hold-tap.mdx @@ -362,6 +362,29 @@ However, if the hold behavior isn't used during fast typing, then it can be an e +#### `exclude-prior-idle-key-positions` + +By default, `require-prior-idle-ms` considers _all_ non-modifier key presses when determining idle time, and applies its quick-tap decision regardless of which key is pressed next. `exclude-prior-idle-key-positions` allows you to specify key positions that are excluded from the prior-idle system entirely: + +- **Before the hold-tap**: Pressing a key in this list cancels the idle timer (equivalent to the timer having already expired), so the next hold-tap activation will go through the normal hold/tap decision process regardless of how recently other keys were pressed. +- **After the hold-tap**: If a key in this list is pressed as the next key in combination with the hold-tap, the quick-tap decision is bypassed and normal hold/tap flavor logic proceeds — as if `require-prior-idle-ms` was never set for that interaction. + +This is useful for home-row mods where opposite-hand keys should never be affected by the idle timer. For example, if your hold-tap is on the left hand, you can exclude all right-hand key positions so that right-hand keys always allow the hold behavior to activate, even during fast typing. + +Note that each hold-tap behavior instance has its own exclude list, so different hold-taps can exclude different positions. + +```dts +rpi: require_prior_idle { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "tap-preferred"; + tapping-term-ms = <200>; + require-prior-idle-ms = <125>; + exclude-prior-idle-key-positions = <3 4 5>; // e.g. opposite-hand keys + bindings = <&kp>, <&kp>; +}; +``` + ### 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.