Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(combos): Add require-prior-idle-ignore #2768

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/dts/bindings/zmk,combos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
48 changes: 41 additions & 7 deletions app/src/combo.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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), \
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
s/.*hid_listener_keycode_//p
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>

/ {
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 = <C LC(D)>;
};
};

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)
>;
};
17 changes: 9 additions & 8 deletions docs/docs/config/combos.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/docs/keymaps/combos.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading