/* * Copyright (c) 2020 The ZMK Contributors * * SPDX-License-Identifier: MIT */ #define DT_DRV_COMPAT zmk_combos #include #include #include #include #include #include #include #include #include #include #include #include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) struct combo_cfg { int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; 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; // the virtual key position is a key position outside the range used by the keyboard. // it is necessary so hold-taps can uniquely identify a behavior. int32_t virtual_key_position; int32_t layers_len; int8_t layers[]; }; struct active_combo { struct combo_cfg *combo; // key_positions_pressed is filled with key_positions when the combo is pressed. // The keys are removed from this array when they are released. // Once this array is empty, the behavior is released. uint32_t key_positions_pressed_count; struct zmk_position_state_changed_event key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; }; struct combo_candidate { struct combo_cfg *combo; // the time after which this behavior should be removed from candidates. // by keeping track of when the candidate should be cleared there is no // possibility of accidental releases. int64_t timeout_at; }; uint32_t pressed_keys_count = 0; // set of keys pressed struct zmk_position_state_changed_event pressed_keys[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO] = {}; // the set of candidate combos based on the currently pressed_keys struct combo_candidate candidates[CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY]; // the last candidate that was completely pressed struct combo_cfg *fully_pressed_combo = NULL; // a lookup dict that maps a key position to all combos on that position struct combo_cfg *combo_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY] = {NULL}; // combos that have been activated and still have (some) keys pressed // this array is always contiguous from 0. struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {NULL}; 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) { for (int i = 0; i < new_combo->key_position_len; i++) { int32_t position = new_combo->key_positions[i]; if (position >= ZMK_KEYMAP_LEN) { LOG_ERR("Unable to initialize combo, key position %d does not exist", position); return -EINVAL; } struct combo_cfg *insert_combo = new_combo; bool set = false; for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; j++) { struct combo_cfg *combo_at_j = combo_lookup[position][j]; if (combo_at_j == NULL) { combo_lookup[position][j] = insert_combo; set = true; break; } if (combo_at_j->key_position_len < insert_combo->key_position_len || (combo_at_j->key_position_len == insert_combo->key_position_len && combo_at_j->virtual_key_position < insert_combo->virtual_key_position)) { continue; } // put insert_combo in this spot, move all other combos up. combo_lookup[position][j] = insert_combo; insert_combo = combo_at_j; } if (!set) { LOG_ERR("Too many combos for key position %d, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY %d.", position, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY); return -ENOMEM; } } return 0; } static bool combo_active_on_layer(struct combo_cfg *combo, uint8_t layer) { if (combo->layers[0] == -1) { // -1 in the first layer position is global layer scope return true; } for (int j = 0; j < combo->layers_len; j++) { if (combo->layers[j] == layer) { 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; } 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(); for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { struct combo_cfg *combo = combo_lookup[position][i]; if (combo == NULL) { return number_of_combo_candidates; } 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++; } // LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at); } return number_of_combo_candidates; } static int filter_candidates(int32_t position) { // this code iterates over candidates and the lookup together to filter in O(n) // assuming they are both sorted on key_position_len, virtual_key_position int matches = 0, lookup_idx = 0, candidate_idx = 0; while (lookup_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY && candidate_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) { struct combo_cfg *candidate = candidates[candidate_idx].combo; struct combo_cfg *lookup = combo_lookup[position][lookup_idx]; if (candidate == NULL || lookup == NULL) { break; } if (candidate->virtual_key_position == lookup->virtual_key_position) { candidates[matches] = candidates[candidate_idx]; matches++; candidate_idx++; lookup_idx++; } else if (candidate->key_position_len > lookup->key_position_len) { lookup_idx++; } else if (candidate->key_position_len < lookup->key_position_len) { candidate_idx++; } else if (candidate->virtual_key_position > lookup->virtual_key_position) { lookup_idx++; } else if (candidate->virtual_key_position < lookup->virtual_key_position) { candidate_idx++; } } // clear unmatched candidates for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { candidates[i].combo = NULL; } // LOG_DBG("combo matches after filter %d", matches); return matches; } static int64_t first_candidate_timeout() { int64_t first_timeout = LONG_MAX; for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { if (candidates[i].combo == NULL) { break; } if (candidates[i].timeout_at < first_timeout) { first_timeout = candidates[i].timeout_at; } } return first_timeout; } static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate) { // this code assumes set(pressed_keys) <= set(candidate->key_positions) // this invariant is enforced by filter_candidates // since events may have been reraised after clearing one or more slots at // the start of pressed_keys (see: release_pressed_keys), we have to check // that each key needed to trigger the combo was pressed, not just the last. return candidate->key_position_len == pressed_keys_count; } static int cleanup(); static int filter_timed_out_candidates(int64_t timestamp) { int remaining_candidates = 0; for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { struct combo_candidate *candidate = &candidates[i]; if (candidate->combo == NULL) { break; } if (candidate->timeout_at > timestamp) { bool need_to_bubble_up = remaining_candidates != i; if (need_to_bubble_up) { // bubble up => reorder candidates so they're contiguous candidates[remaining_candidates].combo = candidate->combo; candidates[remaining_candidates].timeout_at = candidate->timeout_at; // clear the previous location candidates[i].combo = NULL; candidates[i].timeout_at = 0; } remaining_candidates++; } else { candidate->combo = NULL; } } LOG_DBG( "after filtering out timed out combo candidates: remaining_candidates=%d timestamp=%lld", remaining_candidates, timestamp); return remaining_candidates; } static int clear_candidates() { for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { if (candidates[i].combo == NULL) { return i; } candidates[i].combo = NULL; } return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; } static int capture_pressed_key(const struct zmk_position_state_changed *ev) { if (pressed_keys_count == CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) { return ZMK_EV_EVENT_BUBBLE; } pressed_keys[pressed_keys_count++] = copy_raised_zmk_position_state_changed(ev); return ZMK_EV_EVENT_CAPTURED; } const struct zmk_listener zmk_listener_combo; static int release_pressed_keys() { uint32_t count = pressed_keys_count; pressed_keys_count = 0; for (int i = 0; i < count; i++) { struct zmk_position_state_changed_event *ev = &pressed_keys[i]; if (i == 0) { LOG_DBG("combo: releasing position event %d", ev->data.position); ZMK_EVENT_RELEASE(*ev); } else { // reprocess events (see tests/combo/fully-overlapping-combos-3 for why this is needed) LOG_DBG("combo: reraising position event %d", ev->data.position); ZMK_EVENT_RAISE(*ev); } } return count; } static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { struct zmk_behavior_binding_event event = { .position = combo->virtual_key_position, .timestamp = timestamp, #if IS_ENABLED(CONFIG_ZMK_SPLIT) .source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL, #endif }; last_combo_timestamp = timestamp; return zmk_behavior_invoke_binding(&combo->behavior, event, true); } static inline int release_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { struct zmk_behavior_binding_event event = { .position = combo->virtual_key_position, .timestamp = timestamp, #if IS_ENABLED(CONFIG_ZMK_SPLIT) .source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL, #endif }; return zmk_behavior_invoke_binding(&combo->behavior, event, false); } static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) { int combo_length = MIN(pressed_keys_count, active_combo->combo->key_position_len); for (int i = 0; i < combo_length; i++) { active_combo->key_positions_pressed[i] = pressed_keys[i]; } active_combo->key_positions_pressed_count = combo_length; // move any other pressed keys up for (int i = 0; i + combo_length < pressed_keys_count; i++) { pressed_keys[i] = pressed_keys[i + combo_length]; } pressed_keys_count -= combo_length; } static struct active_combo *store_active_combo(struct combo_cfg *combo) { for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) { if (active_combos[i].combo == NULL) { active_combos[i].combo = combo; active_combo_count++; return &active_combos[i]; } } LOG_ERR("Unable to store combo; already %d active. Increase " "CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS", CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS); return NULL; } static void activate_combo(struct combo_cfg *combo) { struct active_combo *active_combo = store_active_combo(combo); if (active_combo == NULL) { // unable to store combo release_pressed_keys(); return; } move_pressed_keys_to_active_combo(active_combo); press_combo_behavior(combo, active_combo->key_positions_pressed[0].data.timestamp); } static void deactivate_combo(int active_combo_index) { active_combo_count--; if (active_combo_index != active_combo_count) { memcpy(&active_combos[active_combo_index], &active_combos[active_combo_count], sizeof(struct active_combo)); } active_combos[active_combo_count].combo = NULL; active_combos[active_combo_count] = (struct active_combo){0}; } /* returns true if a key was released. */ static bool release_combo_key(int32_t position, int64_t timestamp) { for (int combo_idx = 0; combo_idx < active_combo_count; combo_idx++) { struct active_combo *active_combo = &active_combos[combo_idx]; bool key_released = false; bool all_keys_pressed = active_combo->key_positions_pressed_count == active_combo->combo->key_position_len; bool all_keys_released = true; for (int i = 0; i < active_combo->key_positions_pressed_count; i++) { if (key_released) { active_combo->key_positions_pressed[i - 1] = active_combo->key_positions_pressed[i]; all_keys_released = false; } else if (active_combo->key_positions_pressed[i].data.position != position) { all_keys_released = false; } else { // position matches key_released = true; } } if (key_released) { active_combo->key_positions_pressed_count--; if ((active_combo->combo->slow_release && all_keys_released) || (!active_combo->combo->slow_release && all_keys_pressed)) { release_combo_behavior(active_combo->combo, timestamp); } if (all_keys_released) { deactivate_combo(combo_idx); } return true; } } return false; } static int cleanup() { k_work_cancel_delayable(&timeout_task); clear_candidates(); if (fully_pressed_combo != NULL) { activate_combo(fully_pressed_combo); fully_pressed_combo = NULL; } return release_pressed_keys(); } static void update_timeout_task() { int64_t first_timeout = first_candidate_timeout(); if (timeout_task_timeout_at == first_timeout) { return; } if (first_timeout == LLONG_MAX) { timeout_task_timeout_at = 0; k_work_cancel_delayable(&timeout_task); return; } if (k_work_schedule(&timeout_task, K_MSEC(first_timeout - k_uptime_get())) >= 0) { timeout_task_timeout_at = first_timeout; } } static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_changed *data) { int num_candidates; if (candidates[0].combo == NULL) { num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp); if (num_candidates == 0) { return ZMK_EV_EVENT_BUBBLE; } } else { filter_timed_out_candidates(data->timestamp); num_candidates = filter_candidates(data->position); } update_timeout_task(); struct combo_cfg *candidate_combo = candidates[0].combo; LOG_DBG("combo: capturing position event %d", data->position); int ret = capture_pressed_key(data); switch (num_candidates) { case 0: cleanup(); return ret; case 1: if (candidate_is_completely_pressed(candidate_combo)) { fully_pressed_combo = candidate_combo; cleanup(); } return ret; default: if (candidate_is_completely_pressed(candidate_combo)) { fully_pressed_combo = candidate_combo; } return ret; } } static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) { int released_keys = cleanup(); if (release_combo_key(data->position, data->timestamp)) { return ZMK_EV_EVENT_HANDLED; } if (released_keys > 1) { // The second and further key down events are re-raised. To preserve // correct order for e.g. hold-taps, reraise the key up event too. struct zmk_position_state_changed_event dupe_ev = copy_raised_zmk_position_state_changed(data); ZMK_EVENT_RAISE(dupe_ev); return ZMK_EV_EVENT_CAPTURED; } return ZMK_EV_EVENT_BUBBLE; } static void combo_timeout_handler(struct k_work *item) { if (timeout_task_timeout_at == 0 || k_uptime_get() < timeout_task_timeout_at) { // timer was cancelled or rescheduled. return; } if (filter_timed_out_candidates(timeout_task_timeout_at) == 0) { cleanup(); } update_timeout_task(); } 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 ZMK_EV_EVENT_BUBBLE; } if (data->state) { // keydown return position_state_down(ev, data); } else { // keyup return position_state_up(ev, data); } } 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), \ .virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \ .slow_release = DT_PROP(n, slow_release), \ .layers = DT_PROP(n, layers), \ .layers_len = DT_PROP_LEN(n, layers), \ }; #define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n); DT_INST_FOREACH_CHILD(0, COMBO_INST) static int combo_init(void) { k_work_init_delayable(&timeout_task, combo_timeout_handler); DT_INST_FOREACH_CHILD(0, INITIALIZE_COMBO); return 0; } SYS_INIT(combo_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); #endif