Skip to content

Commit

Permalink
feat(combos): Add require-prior-idle-ignore
Browse files Browse the repository at this point in the history
  • Loading branch information
ssbb committed Jan 15, 2025
1 parent 700e9b2 commit ec0be3e
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 7 deletions.
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
1 change: 1 addition & 0 deletions app/tests/combo/require-prior-idle-ignore/events.patterns
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
s/.*hid_listener_keycode_//p
16 changes: 16 additions & 0 deletions app/tests/combo/require-prior-idle-ignore/keycode_events.snapshot
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
61 changes: 61 additions & 0 deletions app/tests/combo/require-prior-idle-ignore/native_posix_64.keymap
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)
>;
};

0 comments on commit ec0be3e

Please sign in to comment.