/* * Copyright (c) 2020 The ZMK Contributors * * SPDX-License-Identifier: MIT */ #define DT_DRV_COMPAT zmk_behavior_hold_tap #include <device.h> #include <drivers/behavior.h> #include <zmk/keys.h> #include <dt-bindings/zmk/keys.h> #include <logging/log.h> #include <zmk/behavior.h> #include <zmk/matrix.h> #include <zmk/endpoints.h> #include <zmk/event_manager.h> #include <zmk/events/position_state_changed.h> #include <zmk/events/keycode_state_changed.h> #include <zmk/behavior.h> #include <zmk/keymap.h> LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) #define ZMK_BHV_HOLD_TAP_MAX_HELD 10 #define ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS 40 // increase if you have keyboard with more keys. #define ZMK_BHV_HOLD_TAP_POSITION_NOT_USED 9999 enum flavor { FLAVOR_HOLD_PREFERRED, FLAVOR_BALANCED, FLAVOR_TAP_PREFERRED, }; enum status { STATUS_UNDECIDED, STATUS_TAP, STATUS_HOLD_INTERRUPT, STATUS_HOLD_TIMER, }; enum decision_moment { HT_KEY_UP, HT_OTHER_KEY_DOWN, HT_OTHER_KEY_UP, HT_TIMER_EVENT, HT_QUICK_TAP, }; struct behavior_hold_tap_config { int tapping_term_ms; char *hold_behavior_dev; char *tap_behavior_dev; int quick_tap_ms; enum flavor flavor; }; // this data is specific for each hold-tap struct active_hold_tap { int32_t position; uint32_t param_hold; uint32_t param_tap; int64_t timestamp; enum status status; const struct behavior_hold_tap_config *config; struct k_delayed_work work; bool work_is_cancelled; }; // The undecided hold tap is the hold tap that needs to be decided before // other keypress events can be released. While the undecided_hold_tap is // not NULL, most events are captured in captured_events. // After the hold_tap is decided, it will stay in the active_hold_taps until // its key-up has been processed and the delayed work is cleaned up. struct active_hold_tap *undecided_hold_tap = NULL; struct active_hold_tap active_hold_taps[ZMK_BHV_HOLD_TAP_MAX_HELD] = {}; // We capture most position_state_changed events and some modifiers_state_changed events. const zmk_event_t *captured_events[ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS] = {}; // Keep track of which key was tapped most recently for 'quick_tap_ms' struct last_tapped { int32_t position; int64_t tap_deadline; }; struct last_tapped last_tapped; static void store_last_tapped(struct active_hold_tap *hold_tap) { last_tapped.position = hold_tap->position; last_tapped.tap_deadline = hold_tap->timestamp + hold_tap->config->quick_tap_ms; } static bool is_quick_tap(struct active_hold_tap *hold_tap) { return last_tapped.position == hold_tap->position && last_tapped.tap_deadline > hold_tap->timestamp; } static int capture_event(const zmk_event_t *event) { for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) { if (captured_events[i] == NULL) { captured_events[i] = event; return 0; } } return -ENOMEM; } static struct zmk_position_state_changed *find_captured_keydown_event(uint32_t position) { struct zmk_position_state_changed *last_match = NULL; for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) { const zmk_event_t *eh = captured_events[i]; if (eh == NULL) { return last_match; } struct zmk_position_state_changed *position_event = as_zmk_position_state_changed(eh); if (position_event == NULL) { continue; } if (position_event->position == position && position_event->state) { last_match = position_event; } } return last_match; } const struct zmk_listener zmk_listener_behavior_hold_tap; static void release_captured_events() { if (undecided_hold_tap != NULL) { return; } // We use a trick to prevent copying the captured_events array. // // Events for different mod-tap instances are separated by a NULL pointer. // // The first event popped will never be catched by the next active hold-tap // because to start capturing a mod-tap-key-down event must first completely // go through the events queue. // // Example of this release process; // [mt2_down, k1_down, k1_up, mt2_up, null, ...] // ^ // mt2_down position event isn't captured because no hold-tap is active. // mt2_down behavior event is handled, now we have an undecided hold-tap // [null, k1_down, k1_up, mt2_up, null, ...] // ^ // k1_down is captured by the mt2 mod-tap // !note that searches for find_captured_keydown_event by the mt2 behavior will stop at the // first null encountered [mt1_down, null, k1_up, mt2_up, null, ...] // ^ // k1_up event is captured by the new hold-tap: // [k1_down, k1_up, null, mt2_up, null, ...] // ^ // mt2_up event is not captured but causes release of mt2 behavior // [k1_down, k1_up, null, null, null, ...] // now mt2 will start releasing it's own captured positions. for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) { const zmk_event_t *captured_event = captured_events[i]; if (captured_event == NULL) { return; } captured_events[i] = NULL; if (undecided_hold_tap != NULL) { k_msleep(10); } struct zmk_position_state_changed *position_event; struct zmk_keycode_state_changed *modifier_event; if ((position_event = as_zmk_position_state_changed(captured_event)) != NULL) { LOG_DBG("Releasing key position event for position %d %s", position_event->position, (position_event->state ? "pressed" : "released")); } else if ((modifier_event = as_zmk_keycode_state_changed(captured_event)) != NULL) { LOG_DBG("Releasing mods changed event 0x%02X %s", modifier_event->keycode, (modifier_event->state ? "pressed" : "released")); } ZMK_EVENT_RAISE_AT(captured_event, behavior_hold_tap); } } static struct active_hold_tap *find_hold_tap(uint32_t position) { for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_HELD; i++) { if (active_hold_taps[i].position == position) { return &active_hold_taps[i]; } } return NULL; } static struct active_hold_tap *store_hold_tap(uint32_t position, uint32_t param_hold, uint32_t param_tap, int64_t timestamp, const struct behavior_hold_tap_config *config) { 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; } active_hold_taps[i].position = position; active_hold_taps[i].status = STATUS_UNDECIDED; active_hold_taps[i].config = config; active_hold_taps[i].param_hold = param_hold; active_hold_taps[i].param_tap = param_tap; active_hold_taps[i].timestamp = timestamp; return &active_hold_taps[i]; } return NULL; } static void clear_hold_tap(struct active_hold_tap *hold_tap) { hold_tap->position = ZMK_BHV_HOLD_TAP_POSITION_NOT_USED; hold_tap->status = STATUS_UNDECIDED; hold_tap->work_is_cancelled = false; } static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_moment event) { switch (event) { case HT_KEY_UP: hold_tap->status = STATUS_TAP; return; case HT_OTHER_KEY_UP: hold_tap->status = STATUS_HOLD_INTERRUPT; return; case HT_TIMER_EVENT: hold_tap->status = STATUS_HOLD_TIMER; return; case HT_QUICK_TAP: hold_tap->status = STATUS_TAP; return; default: return; } } static void decide_tap_preferred(struct active_hold_tap *hold_tap, enum decision_moment event) { switch (event) { case HT_KEY_UP: hold_tap->status = STATUS_TAP; return; case HT_TIMER_EVENT: hold_tap->status = STATUS_HOLD_TIMER; return; case HT_QUICK_TAP: hold_tap->status = STATUS_TAP; return; default: return; } } static void decide_hold_preferred(struct active_hold_tap *hold_tap, enum decision_moment event) { switch (event) { case HT_KEY_UP: hold_tap->status = STATUS_TAP; return; case HT_OTHER_KEY_DOWN: hold_tap->status = STATUS_HOLD_INTERRUPT; return; case HT_TIMER_EVENT: hold_tap->status = STATUS_HOLD_TIMER; return; case HT_QUICK_TAP: hold_tap->status = STATUS_TAP; return; default: return; } } static inline const char *flavor_str(enum flavor flavor) { switch (flavor) { case FLAVOR_HOLD_PREFERRED: return "hold-preferred"; case FLAVOR_BALANCED: return "balanced"; case FLAVOR_TAP_PREFERRED: return "tap-preferred"; } return "UNKNOWN FLAVOR"; } static inline const char *status_str(enum status status) { switch (status) { case STATUS_UNDECIDED: return "undecided"; case STATUS_HOLD_TIMER: return "hold-timer"; case STATUS_HOLD_INTERRUPT: return "hold-interrupt"; case STATUS_TAP: return "tap"; } return "UNKNOWN STATUS"; } static inline const char *decision_moment_str(enum decision_moment decision_moment) { switch (decision_moment) { case HT_KEY_UP: return "key-up"; case HT_OTHER_KEY_DOWN: return "other-key-down"; case HT_OTHER_KEY_UP: return "other-key-up"; case HT_QUICK_TAP: return "quick-tap"; case HT_TIMER_EVENT: return "timer"; } return "UNKNOWN STATUS"; } static int press_binding(struct active_hold_tap *hold_tap) { struct zmk_behavior_binding_event event = { .position = hold_tap->position, .timestamp = hold_tap->timestamp, }; struct zmk_behavior_binding binding = {0}; if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { binding.behavior_dev = hold_tap->config->hold_behavior_dev; binding.param1 = hold_tap->param_hold; } else { binding.behavior_dev = hold_tap->config->tap_behavior_dev; binding.param1 = hold_tap->param_tap; store_last_tapped(hold_tap); } return behavior_keymap_binding_pressed(&binding, event); } static int release_binding(struct active_hold_tap *hold_tap) { struct zmk_behavior_binding_event event = { .position = hold_tap->position, .timestamp = hold_tap->timestamp, }; struct zmk_behavior_binding binding = {0}; if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { binding.behavior_dev = hold_tap->config->hold_behavior_dev; binding.param1 = hold_tap->param_hold; } else { binding.behavior_dev = hold_tap->config->tap_behavior_dev; binding.param1 = hold_tap->param_tap; } return behavior_keymap_binding_released(&binding, event); } static void decide_hold_tap(struct active_hold_tap *hold_tap, enum decision_moment decision_moment) { if (hold_tap->status != STATUS_UNDECIDED) { return; } if (hold_tap != undecided_hold_tap) { LOG_DBG("ERROR found undecided tap hold that is not the active tap hold"); return; } switch (hold_tap->config->flavor) { case FLAVOR_HOLD_PREFERRED: decide_hold_preferred(hold_tap, decision_moment); case FLAVOR_BALANCED: decide_balanced(hold_tap, decision_moment); case FLAVOR_TAP_PREFERRED: decide_tap_preferred(hold_tap, decision_moment); } if (hold_tap->status == STATUS_UNDECIDED) { return; } LOG_DBG("%d decided %s (%s decision moment %s)", hold_tap->position, status_str(hold_tap->status), flavor_str(hold_tap->config->flavor), decision_moment_str(decision_moment)); undecided_hold_tap = NULL; press_binding(hold_tap); release_captured_events(); } static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event) { const struct device *dev = device_get_binding(binding->behavior_dev); const struct behavior_hold_tap_config *cfg = dev->config; if (undecided_hold_tap != NULL) { LOG_DBG("ERROR another hold-tap behavior is undecided."); // if this happens, make sure the behavior events occur AFTER other position events. return ZMK_BEHAVIOR_OPAQUE; } struct active_hold_tap *hold_tap = store_hold_tap(event.position, binding->param1, binding->param2, event.timestamp, cfg); if (hold_tap == NULL) { LOG_ERR("unable to store hold-tap info, did you press more than %d hold-taps?", ZMK_BHV_HOLD_TAP_MAX_HELD); return ZMK_BEHAVIOR_OPAQUE; } LOG_DBG("%d new undecided hold_tap", event.position); undecided_hold_tap = hold_tap; if (is_quick_tap(hold_tap)) { decide_hold_tap(hold_tap, HT_QUICK_TAP); } // if this behavior was queued we have to adjust the timer to only // wait for the remaining time. int32_t tapping_term_ms_left = (hold_tap->timestamp + cfg->tapping_term_ms) - k_uptime_get(); if (tapping_term_ms_left > 0) { k_delayed_work_submit(&hold_tap->work, K_MSEC(tapping_term_ms_left)); } return ZMK_BEHAVIOR_OPAQUE; } static int on_hold_tap_binding_released(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event) { struct active_hold_tap *hold_tap = find_hold_tap(event.position); if (hold_tap == NULL) { LOG_ERR("ACTIVE_HOLD_TAP_CLEANED_UP_TOO_EARLY"); return ZMK_BEHAVIOR_OPAQUE; } // 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_delayed_work_cancel(&hold_tap->work); if (event.timestamp > (hold_tap->timestamp + hold_tap->config->tapping_term_ms)) { decide_hold_tap(hold_tap, HT_TIMER_EVENT); } decide_hold_tap(hold_tap, HT_KEY_UP); release_binding(hold_tap); if (work_cancel_result == -EINPROGRESS) { // let the timer handler clean up // if we'd clear now, the timer may call back for an uninitialized active_hold_tap. LOG_DBG("%d hold-tap timer work in event queue", event.position); hold_tap->work_is_cancelled = true; } else { LOG_DBG("%d cleaning up hold-tap", event.position); clear_hold_tap(hold_tap); } return ZMK_BEHAVIOR_OPAQUE; } static const struct behavior_driver_api behavior_hold_tap_driver_api = { .binding_pressed = on_hold_tap_binding_pressed, .binding_released = on_hold_tap_binding_released, }; static int position_state_changed_listener(const zmk_event_t *eh) { struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); if (undecided_hold_tap == NULL) { LOG_DBG("%d bubble (no undecided hold_tap active)", ev->position); return ZMK_EV_EVENT_BUBBLE; } if (undecided_hold_tap->position == ev->position) { if (ev->state) { // keydown LOG_ERR("hold-tap listener should be called before before most other listeners!"); return ZMK_EV_EVENT_BUBBLE; } else { // keyup LOG_DBG("%d bubble undecided hold-tap keyrelease event", undecided_hold_tap->position); 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. if (ev->timestamp > (undecided_hold_tap->timestamp + undecided_hold_tap->config->tapping_term_ms)) { decide_hold_tap(undecided_hold_tap, HT_TIMER_EVENT); } if (!ev->state && find_captured_keydown_event(ev->position) == NULL) { // no keydown event has been captured, let it bubble. // we'll catch modifiers later in modifier_state_changed_listener LOG_DBG("%d bubbling %d %s event", undecided_hold_tap->position, ev->position, ev->state ? "down" : "up"); return ZMK_EV_EVENT_BUBBLE; } LOG_DBG("%d capturing %d %s event", undecided_hold_tap->position, ev->position, ev->state ? "down" : "up"); capture_event(eh); decide_hold_tap(undecided_hold_tap, ev->state ? HT_OTHER_KEY_DOWN : HT_OTHER_KEY_UP); return ZMK_EV_EVENT_CAPTURED; } static int keycode_state_changed_listener(const zmk_event_t *eh) { // we want to catch layer-up events too... how? struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); if (undecided_hold_tap == NULL) { // LOG_DBG("0x%02X bubble (no undecided hold_tap active)", ev->keycode); return ZMK_EV_EVENT_BUBBLE; } if (!is_mod(ev->usage_page, ev->keycode)) { // LOG_DBG("0x%02X bubble (not a mod)", ev->keycode); return ZMK_EV_EVENT_BUBBLE; } // only key-up events will bubble through position_state_changed_listener // if a undecided_hold_tap is active. LOG_DBG("%d capturing 0x%02X %s event", undecided_hold_tap->position, ev->keycode, ev->state ? "down" : "up"); capture_event(eh); return ZMK_EV_EVENT_CAPTURED; } int behavior_hold_tap_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(behavior_hold_tap, behavior_hold_tap_listener); ZMK_SUBSCRIPTION(behavior_hold_tap, zmk_position_state_changed); // this should be modifiers_state_changed, but unfrotunately that's not implemented yet. ZMK_SUBSCRIPTION(behavior_hold_tap, zmk_keycode_state_changed); void behavior_hold_tap_timer_work_handler(struct k_work *item) { struct active_hold_tap *hold_tap = CONTAINER_OF(item, struct active_hold_tap, work); if (hold_tap->work_is_cancelled) { clear_hold_tap(hold_tap); } else { decide_hold_tap(hold_tap, HT_TIMER_EVENT); } } static int behavior_hold_tap_init(const struct device *dev) { static bool init_first_run = true; if (init_first_run) { for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_HELD; i++) { k_delayed_work_init(&active_hold_taps[i].work, behavior_hold_tap_timer_work_handler); active_hold_taps[i].position = ZMK_BHV_HOLD_TAP_POSITION_NOT_USED; } } init_first_run = false; return 0; } struct behavior_hold_tap_data {}; static struct behavior_hold_tap_data behavior_hold_tap_data; #define KP_INST(n) \ static struct behavior_hold_tap_config behavior_hold_tap_config_##n = { \ .tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \ .hold_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 0)), \ .tap_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 1)), \ .quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \ .flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \ }; \ DEVICE_AND_API_INIT(behavior_hold_tap_##n, DT_INST_LABEL(n), behavior_hold_tap_init, \ &behavior_hold_tap_data, &behavior_hold_tap_config_##n, APPLICATION, \ CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_hold_tap_driver_api); DT_INST_FOREACH_STATUS_OKAY(KP_INST) #endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */