From 4476e9bc0ffc7232d801cc0ae0bf77cd0d8a8af9 Mon Sep 17 00:00:00 2001 From: Sviatoslav Bulbakha Date: Wed, 15 Jan 2025 11:06:21 +0400 Subject: [PATCH] feat(combos): Add require-prior-idle-ignore --- app/dts/bindings/zmk,combos.yaml | 3 + app/src/combo.c | 48 ++++++++++++--- .../require-prior-idle-ignore/events.patterns | 1 + .../keycode_events.snapshot | 16 +++++ .../native_posix_64.keymap | 61 +++++++++++++++++++ docs/docs/config/combos.md | 17 +++--- docs/docs/keymaps/combos.md | 2 +- 7 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 app/tests/combo/require-prior-idle-ignore/events.patterns create mode 100644 app/tests/combo/require-prior-idle-ignore/keycode_events.snapshot create mode 100644 app/tests/combo/require-prior-idle-ignore/native_posix_64.keymap diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml index f146ab7a230..7ecb7aa2e66 100644 --- a/app/dts/bindings/zmk,combos.yaml +++ b/app/dts/bindings/zmk,combos.yaml @@ -21,6 +21,9 @@ child-binding: require-prior-idle-ms: type: int default: -1 + require-prior-idle-ignore: + type: array + default: [] slow-release: type: boolean layers: diff --git a/app/src/combo.c b/app/src/combo.c index c3334bdb754..a959a29ca41 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -26,6 +26,12 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) +struct combo_quick_tap_ignore_item { + uint16_t page; + uint32_t id; + uint8_t implicit_modifiers; +}; + struct combo_cfg { int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; int32_t key_position_len; @@ -39,7 +45,9 @@ struct combo_cfg { // it is necessary so hold-taps can uniquely identify a behavior. int32_t virtual_key_position; int32_t layers_len; - int8_t layers[]; + int8_t layers[ZMK_KEYMAP_LAYERS_LEN]; + int32_t quick_tap_ignore_len; + struct combo_quick_tap_ignore_item quick_tap_ignore[]; }; struct active_combo { @@ -78,13 +86,15 @@ struct k_work_delayable timeout_task; int64_t timeout_task_timeout_at; // this keeps track of the last non-combo, non-mod key tap -int64_t last_tapped_timestamp = INT32_MIN; +struct zmk_keycode_state_changed last_tapped_key; + // this keeps track of the last time a combo was pressed int64_t last_combo_timestamp = INT32_MIN; -static void store_last_tapped(int64_t timestamp) { - if (timestamp > last_combo_timestamp) { - last_tapped_timestamp = timestamp; +static void store_last_tapped(struct zmk_keycode_state_changed *ev) { + if (ev->timestamp > last_combo_timestamp) { + last_tapped_key = *ev; + last_tapped_key.implicit_modifiers |= zmk_hid_get_explicit_mods(); } } @@ -138,8 +148,20 @@ static bool combo_active_on_layer(struct combo_cfg *combo, uint8_t layer) { return false; } +static bool is_last_tapped_key_quick_tap_ignored(struct combo_cfg *combo) { + for (int i = 0; i < combo->quick_tap_ignore_len; i++) { + const struct combo_quick_tap_ignore_item *ignore = &combo->quick_tap_ignore[i]; + if (ignore->page == last_tapped_key.usage_page && ignore->id == last_tapped_key.keycode && + ignore->implicit_modifiers == last_tapped_key.implicit_modifiers) { + return true; + } + } + return false; +} + static bool is_quick_tap(struct combo_cfg *combo, int64_t timestamp) { - return (last_tapped_timestamp + combo->require_prior_idle_ms) > timestamp; + return ((last_tapped_key.timestamp + combo->require_prior_idle_ms) > timestamp) && + !is_last_tapped_key_quick_tap_ignored(combo); } static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) { @@ -502,7 +524,7 @@ static int position_state_changed_listener(const zmk_event_t *ev) { static int keycode_state_changed_listener(const zmk_event_t *eh) { struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); if (ev->state && !is_mod(ev->usage_page, ev->keycode)) { - store_last_tapped(ev->timestamp); + store_last_tapped(ev); } return ZMK_EV_EVENT_BUBBLE; } @@ -520,6 +542,15 @@ ZMK_LISTENER(combo, behavior_combo_listener); ZMK_SUBSCRIPTION(combo, zmk_position_state_changed); ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed); +#define PARSE_QUICK_TAP_IGNORE_ITEM(i) \ + { \ + .page = ZMK_HID_USAGE_PAGE(i), .id = ZMK_HID_USAGE_ID(i), \ + .implicit_modifiers = SELECT_MODS(i) \ + } + +#define QUICK_TAP_IGNORE_ITEM(i, n) \ + PARSE_QUICK_TAP_IGNORE_ITEM(DT_PROP_BY_IDX(n, require_prior_idle_ignore, i)) + #define COMBO_INST(n) \ static struct combo_cfg combo_config_##n = { \ .timeout_ms = DT_PROP(n, timeout_ms), \ @@ -531,6 +562,9 @@ ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed); .slow_release = DT_PROP(n, slow_release), \ .layers = DT_PROP(n, layers), \ .layers_len = DT_PROP_LEN(n, layers), \ + .quick_tap_ignore = {LISTIFY(DT_PROP_LEN(n, require_prior_idle_ignore), \ + QUICK_TAP_IGNORE_ITEM, (, ), n)}, \ + .quick_tap_ignore_len = DT_PROP_LEN(n, require_prior_idle_ignore), \ }; #define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n); diff --git a/app/tests/combo/require-prior-idle-ignore/events.patterns b/app/tests/combo/require-prior-idle-ignore/events.patterns new file mode 100644 index 00000000000..b1342af4d97 --- /dev/null +++ b/app/tests/combo/require-prior-idle-ignore/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/require-prior-idle-ignore/keycode_events.snapshot b/app/tests/combo/require-prior-idle-ignore/keycode_events.snapshot new file mode 100644 index 00000000000..62e2475e97a --- /dev/null +++ b/app/tests/combo/require-prior-idle-ignore/keycode_events.snapshot @@ -0,0 +1,16 @@ +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0xE0 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0xE0 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/require-prior-idle-ignore/native_posix_64.keymap b/app/tests/combo/require-prior-idle-ignore/native_posix_64.keymap new file mode 100644 index 00000000000..c458a60b597 --- /dev/null +++ b/app/tests/combo/require-prior-idle-ignore/native_posix_64.keymap @@ -0,0 +1,61 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + + combo_ab { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp X>; + require-prior-idle-ms = <100>; + require-prior-idle-ignore = ; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &kp D + &kp LCTL &kp LGUI + >; + }; + }; +}; + +&kscan { + rows = <3>; + columns = <2>; + events = < + // Tap C and then combo, should actuate + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,100) + + // Tap D and then combo, should NOT actuate + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,100) + + // Tap LC(D) and then combo, should actuate + ZMK_MOCK_PRESS(2,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(2,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,100) + >; +}; diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index d773628db2a..fb4f173549b 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -31,13 +31,14 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `bindings` | phandle-array | A [behavior](../keymaps/index.mdx#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| --------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `bindings` | phandle-array | A [behavior](../keymaps/index.mdx#behaviors) to run when the combo is triggered | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | +| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | +| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | +| `require-prior-idle-ignore` | array | A list of keycodes to be ignored by `require-prior-idle-ms` | `<>` | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | +| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/keymaps/combos.md b/docs/docs/keymaps/combos.md index 9616b5eb082..bb6edc2d10a 100644 --- a/docs/docs/keymaps/combos.md +++ b/docs/docs/keymaps/combos.md @@ -30,7 +30,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. -- (advanced) you can specify a `require-prior-idle-ms` value much like for [hold-taps](behaviors/hold-tap.mdx#require-prior-idle-ms). If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the combo will not trigger. +- (advanced) you can specify a `require-prior-idle-ms` value much like for [hold-taps](behaviors/hold-tap.mdx#require-prior-idle-ms). If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the combo will not trigger. You can ignore specific keycodes with `require-prior-idle-ignore`. :::info