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(behaviors): Adding require-prior-idle-ms for combos and hold-taps #1387

Merged
merged 8 commits into from
Oct 3, 2023
2 changes: 1 addition & 1 deletion app/boards/shields/cradio/cradio.keymap
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
flavor = "tap-preferred";
tapping-term-ms = <220>;
quick-tap-ms = <150>;
global-quick-tap;
require-prior-idle-ms = <100>;
bindings = <&kp>, <&kp>;
};
};
Expand Down
10 changes: 8 additions & 2 deletions app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ properties:
required: true
tapping-term-ms:
type: int
tapping_term_ms: # deprecated
tapping_term_ms:
type: int
deprecated: true
quick-tap-ms:
type: int
default: -1
quick_tap_ms: # deprecated
quick_tap_ms:
type: int
deprecated: true
global-quick-tap:
type: boolean
deprecated: true
require-prior-idle-ms:
type: int
default: -1
flavor:
type: string
required: false
Expand Down
3 changes: 3 additions & 0 deletions app/dts/bindings/zmk,combos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ child-binding:
timeout-ms:
type: int
default: 50
require-prior-idle-ms:
type: int
default: -1
slow-release:
type: boolean
layers:
Expand Down
17 changes: 11 additions & 6 deletions app/src/behaviors/behavior_hold_tap.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ struct behavior_hold_tap_config {
char *hold_behavior_dev;
char *tap_behavior_dev;
int quick_tap_ms;
bool global_quick_tap;
int require_prior_idle_ms;
enum flavor flavor;
bool retro_tap;
bool hold_trigger_on_release;
Expand Down Expand Up @@ -97,7 +97,9 @@ struct last_tapped {
int64_t timestamp;
};

struct last_tapped last_tapped = {INT32_MIN, INT64_MIN};
// Set time stamp to large negative number initially for test suites, but not
// int64 min since it will overflow if -1 is added
struct last_tapped last_tapped = {INT32_MIN, INT32_MIN};

static void store_last_tapped(int64_t timestamp) {
if (timestamp > last_tapped.timestamp) {
Expand All @@ -112,10 +114,11 @@ static void store_last_hold_tapped(struct active_hold_tap *hold_tap) {
}

static bool is_quick_tap(struct active_hold_tap *hold_tap) {
if (hold_tap->config->global_quick_tap || last_tapped.position == hold_tap->position) {
return (last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp;
if ((last_tapped.timestamp + hold_tap->config->require_prior_idle_ms) > hold_tap->timestamp) {
return true;
} else {
return false;
return (last_tapped.position == hold_tap->position) &&
(last_tapped.timestamp + hold_tap->config->quick_tap_ms) > hold_tap->timestamp;
}
}

Expand Down Expand Up @@ -703,7 +706,9 @@ static int behavior_hold_tap_init(const struct device *dev) {
.hold_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 0), label), \
.tap_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 1), label), \
.quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \
.global_quick_tap = DT_INST_PROP(n, global_quick_tap), \
.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), \
.flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \
.retro_tap = DT_INST_PROP(n, retro_tap), \
.hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \
Expand Down
50 changes: 44 additions & 6 deletions app/src/combo.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <zmk/behavior.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/keycode_state_changed.h>
#include <zmk/hid.h>
#include <zmk/matrix.h>
#include <zmk/keymap.h>
Expand All @@ -30,6 +31,7 @@ struct combo_cfg {
int32_t key_position_len;
struct zmk_behavior_binding behavior;
int32_t timeout_ms;
int32_t require_prior_idle_ms;
// if slow release is set, the combo releases when the last key is released.
// otherwise, the combo releases when the first key is released.
bool slow_release;
Expand Down Expand Up @@ -72,6 +74,17 @@ int active_combo_count = 0;
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;
// 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;
}
}

// Store the combo key pointer in the combos array, one pointer for each key position
// The combos are sorted shortest-first, then by virtual-key-position.
static int initialize_combo(struct combo_cfg *new_combo) {
Expand Down Expand Up @@ -122,6 +135,10 @@ static bool combo_active_on_layer(struct combo_cfg *combo, uint8_t layer) {
return false;
}

static bool is_quick_tap(struct combo_cfg *combo, int64_t timestamp) {
return (last_tapped_timestamp + combo->require_prior_idle_ms) > timestamp;
}

static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) {
int number_of_combo_candidates = 0;
uint8_t highest_active_layer = zmk_keymap_highest_layer_active();
Expand All @@ -130,7 +147,7 @@ static int setup_candidates_for_first_keypress(int32_t position, int64_t timesta
if (combo == NULL) {
return number_of_combo_candidates;
}
if (combo_active_on_layer(combo, highest_active_layer)) {
if (combo_active_on_layer(combo, highest_active_layer) && !is_quick_tap(combo, timestamp)) {
candidates[number_of_combo_candidates].combo = combo;
candidates[number_of_combo_candidates].timeout_at = timestamp + combo->timeout_ms;
number_of_combo_candidates++;
Expand Down Expand Up @@ -240,7 +257,7 @@ static int capture_pressed_key(const zmk_event_t *ev) {
pressed_keys[i] = ev;
return ZMK_EV_EVENT_CAPTURED;
}
return 0;
return ZMK_EV_EVENT_BUBBLE;
}

const struct zmk_listener zmk_listener_combo;
Expand Down Expand Up @@ -272,6 +289,8 @@ static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestam
.timestamp = timestamp,
};

last_combo_timestamp = timestamp;

return behavior_keymap_binding_pressed(&combo->behavior, event);
}

Expand Down Expand Up @@ -401,7 +420,7 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_
if (candidates[0].combo == NULL) {
num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp);
if (num_candidates == 0) {
return 0;
return ZMK_EV_EVENT_BUBBLE;
}
} else {
filter_timed_out_candidates(data->timestamp);
Expand Down Expand Up @@ -441,7 +460,7 @@ static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_ch
ZMK_EVENT_RAISE(ev);
return ZMK_EV_EVENT_CAPTURED;
}
return 0;
return ZMK_EV_EVENT_BUBBLE;
}

static void combo_timeout_handler(struct k_work *item) {
Expand All @@ -458,7 +477,7 @@ static void combo_timeout_handler(struct k_work *item) {
static int position_state_changed_listener(const zmk_event_t *ev) {
struct zmk_position_state_changed *data = as_zmk_position_state_changed(ev);
if (data == NULL) {
return 0;
return ZMK_EV_EVENT_BUBBLE;
}

if (data->state) { // keydown
Expand All @@ -468,12 +487,31 @@ static int position_state_changed_listener(const zmk_event_t *ev) {
}
}

ZMK_LISTENER(combo, position_state_changed_listener);
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);
}
return ZMK_EV_EVENT_BUBBLE;
}

int behavior_combo_listener(const zmk_event_t *eh) {
if (as_zmk_position_state_changed(eh) != NULL) {
return position_state_changed_listener(eh);
} else if (as_zmk_keycode_state_changed(eh) != NULL) {
return keycode_state_changed_listener(eh);
}
return ZMK_EV_EVENT_BUBBLE;
}

ZMK_LISTENER(combo, behavior_combo_listener);
ZMK_SUBSCRIPTION(combo, zmk_position_state_changed);
ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed);

#define COMBO_INST(n) \
static struct combo_cfg combo_config_##n = { \
.timeout_ms = DT_PROP(n, timeout_ms), \
.require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \
.key_positions = DT_PROP(n, key_positions), \
.key_position_len = DT_PROP_LEN(n, key_positions), \
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \
Expand Down
1 change: 1 addition & 0 deletions app/tests/combo/require-prior-idle/events.patterns
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
s/.*hid_listener_keycode_//p
14 changes: 14 additions & 0 deletions app/tests/combo/require-prior-idle/keycode_events.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 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 0x1B implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x1B 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 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00
64 changes: 64 additions & 0 deletions app/tests/combo/require-prior-idle/native_posix_64.keymap
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>

/ {
combos {
compatible = "zmk,combos";
combo_one {
timeout-ms = <50>;
key-positions = <0 1>;
bindings = <&kp X>;
require-prior-idle-ms = <100>;
};

combo_two {
timeout-ms = <50>;
key-positions = <0 2>;
bindings = <&kp Y>;
};
};

keymap {
compatible = "zmk,keymap";
label ="Default keymap";

default_layer {
bindings = <
&kp A &kp B
&kp C &kp D
>;
};
};
};

&kscan {
events = <
/* Tap A */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,60)
/* Quick Tap A and B */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_PRESS(0,1,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,1,200)
/* Combo One */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_PRESS(0,1,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,1,10)
/* Combo One Again (shouldn't quick tap) */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_PRESS(0,1,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,1,10)
/* Tap A */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,60)
/* Combo 2 */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(1,0,10)
>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
events = <
/* tap */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,0,250)
/* normal quick tap */
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(0,0,400)
Expand All @@ -16,7 +16,7 @@
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_RELEASE(0,0,400)
/* global quick tap */
/* require-prior-idle */
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(1,0,10)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
flavor = "balanced";
tapping-term-ms = <300>;
quick-tap-ms = <300>;
require-prior-idle-ms = <100>;
bindings = <&kp>, <&kp>;
global-quick-tap;
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
events = <
/* tap */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,0,250)
/* normal quick tap */
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(0,0,400)
Expand All @@ -16,7 +16,7 @@
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_RELEASE(0,0,400)
/* global quick tap */
/* require-prior-idle */
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(1,0,10)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
flavor = "hold-preferred";
tapping-term-ms = <300>;
quick-tap-ms = <300>;
require-prior-idle-ms = <100>;
bindings = <&kp>, <&kp>;
global-quick-tap;
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
events = <
/* tap */
ZMK_MOCK_PRESS(0,0,10)
ZMK_MOCK_RELEASE(0,0,10)
ZMK_MOCK_RELEASE(0,0,250)
/* normal quick tap */
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(0,0,400)
Expand All @@ -16,7 +16,7 @@
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_RELEASE(0,0,400)
/* global quick tap */
/* require-prior-idle */
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_RELEASE(1,0,10)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
&kscan {
events = <
/* hold the first mod tap */
ZMK_MOCK_PRESS(0,0,400)
ZMK_MOCK_PRESS(0,0,10)
/* hold the second mod tap */
ZMK_MOCK_PRESS(0,1,400)
/* press the normal key */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
flavor = "tap-preferred";
tapping-term-ms = <300>;
quick-tap-ms = <300>;
require-prior-idle-ms = <100>;
bindings = <&kp>, <&kp>;
global-quick-tap;
};
};

Expand Down