From 3042c212a95a3f5b2aeeefc9520fc3ad68b79edb Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Sat, 15 Nov 2025 18:31:04 -0700 Subject: [PATCH] feat(combos): Initial runtime combos support Add a new "runtime combo" concept. Much like reserved layers for keymaps, users can add any number of additional combos with `status = "reserved"` to add extra combos that can be created at runtime. Persistance of runtime combos to the settings subsystem will be added in a follow-up commit. Runtime combos are identified by an "id" value, since they may be sorted based on runtime changes to preserve the fast combo expectations when keys are pressed. --- app/CMakeLists.txt | 1 + app/Kconfig | 16 +- app/Kconfig.combos | 44 +++ app/include/zmk/behavior.h | 6 + app/include/zmk/combos.h | 74 ++++- app/src/combo.c | 625 ++++++++++++++++++++++++++++++++++--- app/src/keymap.c | 6 - 7 files changed, 712 insertions(+), 60 deletions(-) create mode 100644 app/Kconfig.combos diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 33c41c084..b69517843 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -126,6 +126,7 @@ if (CONFIG_ZMK_STUDIO_RPC) ${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/core.proto ${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/behaviors.proto ${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/keymap.proto + ${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/combos.proto ) target_include_directories(app PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/app/Kconfig b/app/Kconfig index 490627829..30f787df4 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -434,21 +434,7 @@ endmenu menu "Combo options" -config ZMK_COMBO_MAX_PRESSED_COMBOS - int "Maximum number of currently pressed combos" - default 4 - -config ZMK_COMBO_MAX_COMBOS_PER_KEY - int "Deprecated: Max combos per key" - default 0 - help - Deprecated: Storage for combos is now determined automatically - -config ZMK_COMBO_MAX_KEYS_PER_COMBO - int "Deprecated: Max keys per combo" - default 0 - help - Deprecated: This is now auto-calculated based on `key-positions` in devicetree +rsource "Kconfig.combos" # Combo options endmenu diff --git a/app/Kconfig.combos b/app/Kconfig.combos new file mode 100644 index 000000000..0f0200f0c --- /dev/null +++ b/app/Kconfig.combos @@ -0,0 +1,44 @@ +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +menuconfig ZMK_COMBOS + bool "Combos" + default y + depends on DT_HAS_ZMK_COMBOS_ENABLED + +if ZMK_COMBOS + +config ZMK_COMBO_MAX_PRESSED_COMBOS + int "Maximum number of currently pressed combos" + default 4 + +config ZMK_COMBO_MAX_COMBOS_PER_KEY + int "Deprecated: Max combos per key" + default 0 + help + Deprecated: Storage for combos is now determined automatically + +config ZMK_COMBO_MAX_KEYS_PER_COMBO + int "Deprecated: Max keys per combo" + default 0 + help + Deprecated: This is now auto-calculated based on `key-positions` in devicetree + +config ZMK_COMBOS_RUNTIME + bool "Runtime combo updates" + +if ZMK_COMBOS_RUNTIME + +config ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE + bool "Save/restore runtime combos using settings" + default y + depends on SETTINGS + +config ZMK_COMBOS_RUNTIME_SHELL_CMD + bool "Combos shell command" + default y + depends on SHELL + +endif + +endif diff --git a/app/include/zmk/behavior.h b/app/include/zmk/behavior.h index 5028d3202..eb3d966c0 100644 --- a/app/include/zmk/behavior.h +++ b/app/include/zmk/behavior.h @@ -31,6 +31,12 @@ struct zmk_behavior_binding_event { #endif }; +struct zmk_behavior_binding_setting { + zmk_behavior_local_id_t behavior_local_id; + uint32_t param1; + uint32_t param2; +} __packed; + /** * @brief Get a const struct device* for a behavior from its @p name field. * diff --git a/app/include/zmk/combos.h b/app/include/zmk/combos.h index 644a2b53f..5a7ce0efe 100644 --- a/app/include/zmk/combos.h +++ b/app/include/zmk/combos.h @@ -7,10 +7,82 @@ #pragma once #include +#include #define ZMK_COMBOS_UTIL_ONE(n) +1 -#define ZMK_COMBOS_LEN \ +#define ZMK_COMBOS_FOREACH(_fn) \ + COND_CODE_1(IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME), \ + (DT_FOREACH_CHILD(DT_INST(0, zmk_combos), _fn)), \ + (DT_FOREACH_CHILD_STATUS_OKAY(DT_INST(0, zmk_combos), _fn))) + +#define ZMK_COMBOS_FOREACH_SEP(_fn, _sep) \ + COND_CODE_1(IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME), \ + (DT_FOREACH_CHILD_SEP(DT_INST(0, zmk_combos), _fn, _sep)), \ + (DT_FOREACH_CHILD_STATUS_OKAY_SEP(DT_INST(0, zmk_combos), _fn, _sep))) + +#define ZMK_STATIC_COMBOS_LEN \ COND_CODE_1(DT_HAS_COMPAT_STATUS_OKAY(zmk_combos), \ (0 DT_FOREACH_CHILD_STATUS_OKAY(DT_INST(0, zmk_combos), ZMK_COMBOS_UTIL_ONE)), \ (0)) + +#define ZMK_COMBOS_LEN \ + COND_CODE_1(DT_HAS_COMPAT_STATUS_OKAY(zmk_combos), \ + (0 ZMK_COMBOS_FOREACH(ZMK_COMBOS_UTIL_ONE)), (0)) + +#define COMBOS_KEYS_BYTE_ARRAY(node_id) \ + uint8_t _CONCAT(combo_prop_, node_id)[DT_PROP_LEN_OR(node_id, key_positions, 0)]; + +#define MAX_COMBO_KEYS \ + COND_CODE_1(DT_HAS_COMPAT_STATUS_OKAY(zmk_combos), \ + (sizeof(union {ZMK_COMBOS_FOREACH(COMBOS_KEYS_BYTE_ARRAY)})), (0)) + +struct combo_cfg { + uint16_t key_positions[MAX_COMBO_KEYS]; + int16_t key_position_len; + int16_t require_prior_idle_ms; + int32_t timeout_ms; + uint32_t layer_mask; + struct zmk_behavior_binding behavior; + // 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; +}; + +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME) + +typedef int zmk_combo_runtime_id_t; + +struct zmk_combo_runtime { + zmk_combo_runtime_id_t id; + struct combo_cfg combo; +}; + +bool zmk_combos_check_unsaved_changes(void); +int zmk_combos_reset_settings(void); +int zmk_combos_save_changes(void); +int zmk_combos_discard_changes(void); +// TODO: Document +// Returns non-negative combo ID value on success +// Returns negative errno on error +int zmk_combo_runtime_add_combo(const struct combo_cfg *cfg); +int zmk_combo_runtime_remove_combo(zmk_combo_runtime_id_t combo_id); + +int zmk_combo_runtime_set_combo_binding(zmk_combo_runtime_id_t combo_id, + const struct zmk_behavior_binding *binding); +int zmk_combo_runtime_add_combo_position(zmk_combo_runtime_id_t combo_id, uint16_t position); +int zmk_combo_runtime_remove_combo_position(zmk_combo_runtime_id_t combo_id, uint16_t position); + +int zmk_combo_runtime_clear_combo_layers(zmk_combo_runtime_id_t combo_id); +int zmk_combo_runtime_set_combo_layer(zmk_combo_runtime_id_t combo_id, uint8_t layer, bool enabled); + +int zmk_combo_runtime_set_combo_timeout(zmk_combo_runtime_id_t combo_id, uint16_t timeout); +int zmk_combo_runtime_set_combo_prior_idle(zmk_combo_runtime_id_t combo_id, uint16_t prior_idle); +int zmk_combo_runtime_set_combo_slow_release(zmk_combo_runtime_id_t combo_id, bool enabled); + +int zmk_combo_runtime_get_combos(const struct zmk_combo_runtime **list); +const struct zmk_combo_runtime *zmk_combo_runtime_get_combo(zmk_combo_runtime_id_t combo_id); + +int zmk_combo_runtime_get_free_combos(void); + +#endif diff --git a/app/src/combo.c b/app/src/combo.c index 7d869fa45..fd0117a69 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -27,36 +28,19 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) -#if CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO > 0 +#if !IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME) && CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO > 0 #warning \ "CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO is deprecated, and is auto-calculated from the devicetree now." #endif -#if CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY > 0 +#if !IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME) && CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY > 0 #warning "CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY is deprecated, and is auto-calculated." #endif -#define COMBOS_KEYS_BYTE_ARRAY(node_id) \ - uint8_t _CONCAT(combo_prop_, node_id)[DT_PROP_LEN(node_id, key_positions)]; - -#define MAX_COMBO_KEYS sizeof(union {DT_INST_FOREACH_CHILD(0, COMBOS_KEYS_BYTE_ARRAY)}) - -struct combo_cfg { - int32_t key_positions[MAX_COMBO_KEYS]; - int16_t key_position_len; - int16_t require_prior_idle_ms; - int32_t timeout_ms; - uint32_t layer_mask; - struct zmk_behavior_binding behavior; - // 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; -}; - struct active_combo { uint16_t combo_idx; // key_positions_pressed is filled with key_positions when the combo is pressed. @@ -78,10 +62,10 @@ struct active_combo { COND_CODE_1(IS_EQ(DT_PROP_LEN(n, key_positions), positions), \ ( \ { \ - .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), \ + .timeout_ms = DT_PROP(n, timeout_ms), \ + .require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \ .behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \ .slow_release = DT_PROP(n, slow_release), \ .layer_mask = NODE_PROP_BITMASK(n, layers), \ @@ -89,17 +73,556 @@ struct active_combo { ()) #define COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN(positions, _ignore) \ - DT_INST_FOREACH_CHILD_VARGS(0, COMBO_INST, positions) + DT_INST_FOREACH_CHILD_STATUS_OKAY_VARGS(0, COMBO_INST, positions) // We do some magic here to generate the `combos` array by "key position length", looping // by key position length and on each iteration, only include entries where the `key-positions` // length matches. -// Doing so allows our bitmasks to be "shorted key positions list first" when searching for matches. +// Doing so allows our bitmasks to be "sorted key positions list first" when searching for matches. // `20` is chosen as a reasonable limit, since the theoretical maximum number of keys you might // reasonably press simultaneously with 10 fingers is 20 keys, two keys per finger. static const struct combo_cfg combos[] = { LISTIFY(20, COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN, (), 0)}; +static void reload_combo_lookup(void); +static int initialize_combo(size_t index); + +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME) + +struct combo_settings_storage { + struct combo_settings_storage_core { + int16_t require_prior_idle_ms; + int32_t timeout_ms; + uint32_t layer_mask; + struct zmk_behavior_binding_setting behavior; + uint8_t slow_release; + } core; + /* For settings storage, we store these at the end, to save a bit */ + uint16_t key_positions[MAX_COMBO_KEYS]; +} __packed; + +static struct zmk_combo_runtime runtime_combos[ZMK_COMBOS_LEN]; + +BUILD_ASSERT(ZMK_COMBOS_LEN <= 64, "A maximum of 64 combos is supported for runtime combos"); + +#if ZMK_COMBOS_LEN > 32 +typedef uint64_t combos_runtime_change_mask_t; +#else +typedef uint32_t combos_runtime_change_mask_t; +#endif + +static combos_runtime_change_mask_t runtime_combos_changed_ids; + +int zmk_combo_runtime_get_combos(const struct zmk_combo_runtime **list) { + *list = &runtime_combos[0]; + for (size_t i = 0; i < ZMK_COMBOS_LEN; i++) { + if (runtime_combos[i].combo.key_position_len == 0) { + return i; + } + } + + return ZMK_COMBOS_LEN; +} + +static int compare_key_positions(const void *a, const void *b) { + const int16_t *kp_a = a; + const int16_t *kp_b = b; + + return (*kp_a) - (*kp_b); +} + +static int compare_combos_by_kp_len_and_kp(const void *a, const void *b) { + const struct zmk_combo_runtime *c_a = a; + const struct zmk_combo_runtime *c_b = b; + + if (!c_a->combo.key_position_len) { + return INT32_MAX; + } else if (!c_b->combo.key_position_len) { + return INT32_MIN; + } else if (c_a->combo.key_position_len != c_b->combo.key_position_len) { + return c_a->combo.key_position_len - c_b->combo.key_position_len; + } + + for (size_t i = 0; i < c_a->combo.key_position_len; i++) { + if (c_a->combo.key_positions[i] != c_b->combo.key_positions[i]) { + return c_a->combo.key_positions[i] - c_b->combo.key_positions[i]; + } + } + + return 0; +} + +void reindex_combos(void) { + qsort(&runtime_combos, ZMK_COMBOS_LEN, sizeof(runtime_combos[0]), + compare_combos_by_kp_len_and_kp); + + reload_combo_lookup(); +} + +static void mark_combo_changed(zmk_combo_runtime_id_t combo_id) { + WRITE_BIT(runtime_combos_changed_ids, combo_id, true); +} + +static int find_runtime_idx(zmk_combo_runtime_id_t combo_id) { + for (int i = 0; i < ARRAY_SIZE(runtime_combos); i++) { + if (runtime_combos[i].id == combo_id) { + return i; + } + } + + return -EINVAL; +} + +static int add_position_to_runtime_combo(struct zmk_combo_runtime *rc, uint16_t position) { + __ASSERT(rc != NULL, "Passed a NULL combo"); + + if (rc->combo.key_position_len >= MAX_COMBO_KEYS) { + return -ENOMEM; + } + + // Return success if the position is already enabled; + for (size_t i = 0; i < rc->combo.key_position_len; i++) { + if (rc->combo.key_positions[i] == position) { + return 0; + } + } + + rc->combo.key_positions[rc->combo.key_position_len++] = position; + qsort(&rc->combo.key_positions, rc->combo.key_position_len, sizeof(rc->combo.key_positions[0]), + compare_key_positions); + + mark_combo_changed(rc->id); + reindex_combos(); + + return 0; +} + +int zmk_combo_runtime_add_combo(const struct combo_cfg *cfg) { + for (int c = 0; c < ARRAY_SIZE(runtime_combos); c++) { + if (runtime_combos[c].combo.key_position_len == 0) { + memcpy(&runtime_combos[c].combo, cfg, sizeof(struct combo_cfg)); + + zmk_combo_runtime_id_t id = runtime_combos[c].id; + reindex_combos(); + + mark_combo_changed(id); + + return id; + } + } + + return -ENOMEM; +} + +static struct zmk_combo_runtime *get_combo_for_id(zmk_combo_runtime_id_t combo_id) { + int combo_idx = find_runtime_idx(combo_id); + + if (combo_idx < 0 || combo_idx >= ARRAY_SIZE(runtime_combos)) { + return NULL; + } + + return &runtime_combos[combo_idx]; +} +const struct zmk_combo_runtime *zmk_combo_runtime_get_combo(zmk_combo_runtime_id_t combo_id) { + return get_combo_for_id(combo_id); +} + +int zmk_combo_runtime_set_combo_binding(zmk_combo_runtime_id_t combo_id, + const struct zmk_behavior_binding *binding) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + rc->combo.behavior = *binding; + mark_combo_changed(rc->id); + return 0; +} + +int zmk_combo_runtime_add_combo_position(zmk_combo_runtime_id_t combo_id, uint16_t position) { + int ret; + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + ret = add_position_to_runtime_combo(rc, position); + if (ret < 0) { + return ret; + } + + mark_combo_changed(rc->id); + + return 0; +} + +int zmk_combo_runtime_remove_combo_position(zmk_combo_runtime_id_t combo_id, uint16_t position) { + ; + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + for (size_t i = 0; i < rc->combo.key_position_len; i++) { + if (rc->combo.key_positions[i] == position) { + size_t to_move = rc->combo.key_position_len - i - 1; + if (to_move > 0) { + memmove(&rc->combo.key_positions[i], &rc->combo.key_positions[i + 1], + to_move * sizeof(rc->combo.key_positions[0])); + } + + rc->combo.key_position_len--; + + reindex_combos(); + mark_combo_changed(rc->id); + + return 0; + } + } + + return -ENODEV; +} + +int zmk_combo_runtime_set_combo_layer(zmk_combo_runtime_id_t combo_id, uint8_t layer, + bool enabled) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + if (rc->combo.key_position_len == 0) { + return -EINVAL; + } + + WRITE_BIT(rc->combo.layer_mask, layer, enabled); + mark_combo_changed(rc->id); + return 0; +} + +int zmk_combo_runtime_clear_combo_layers(zmk_combo_runtime_id_t combo_id) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + if (rc->combo.key_position_len == 0) { + return -EINVAL; + } + + rc->combo.layer_mask = 0; + mark_combo_changed(rc->id); + + return 0; +} + +int zmk_combo_runtime_set_combo_timeout(zmk_combo_runtime_id_t combo_id, uint16_t timeout) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + if (timeout <= 0) { + return -EINVAL; + } + + if (rc->combo.key_position_len == 0) { + return -EINVAL; + } + + rc->combo.timeout_ms = timeout; + mark_combo_changed(rc->id); + + return 0; +} + +int zmk_combo_runtime_set_combo_prior_idle(zmk_combo_runtime_id_t combo_id, uint16_t prior_idle) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + if (prior_idle < 0) { + return -EINVAL; + } + + if (rc->combo.key_position_len == 0) { + return -EINVAL; + } + + rc->combo.require_prior_idle_ms = prior_idle; + mark_combo_changed(rc->id); + + return 0; +} + +int zmk_combo_runtime_set_combo_slow_release(zmk_combo_runtime_id_t combo_id, bool enabled) { + struct zmk_combo_runtime *rc = get_combo_for_id(combo_id); + if (!rc) { + return -EINVAL; + } + + if (rc->combo.key_position_len == 0) { + return -EINVAL; + } + + rc->combo.slow_release = enabled; + mark_combo_changed(rc->id); + + return 0; +} + +int zmk_combo_runtime_remove_combo(zmk_combo_runtime_id_t combo_id) { + int combo_idx = find_runtime_idx(combo_id); + LOG_DBG("Removing %d at %d", combo_id, combo_idx); + + if (combo_idx < 0 || combo_idx >= ARRAY_SIZE(runtime_combos)) { + return -EINVAL; + } + + if (runtime_combos[combo_idx].combo.key_position_len == 0) { + return -EINVAL; + } + + if (combo_idx == ZMK_COMBOS_LEN - 1) { + memset(&runtime_combos[combo_idx].combo, 0, sizeof(struct combo_cfg)); + LOG_DBG("index %d has id %d", combo_idx, runtime_combos[combo_idx].id); + + } else { + for (size_t i = ZMK_COMBOS_LEN - 1; i >= combo_id; i--) { + if (runtime_combos[i].combo.key_position_len > 0) { + memmove(&runtime_combos[combo_idx], &runtime_combos[combo_idx + 1], + (i - combo_idx) * sizeof(runtime_combos[0])); + memset(&runtime_combos[i].combo, 0, sizeof(struct combo_cfg)); + /* Ensure the removed ID isn't dropped, just placed at the end of the list */ + runtime_combos[i].id = combo_id; + break; + } + } + } + + LOG_DBG("index %d has id %d", combo_idx, runtime_combos[combo_idx].id); + reindex_combos(); + LOG_DBG("index %d has id %d", combo_idx, runtime_combos[combo_idx].id); + LOG_DBG("Marking %d as changed", combo_id); + mark_combo_changed(combo_id); + + return 0; +} + +int zmk_combo_runtime_get_free_combos(void) { + int ret = 0; + + for (int i = ARRAY_SIZE(runtime_combos) - 1; i >= 0; i--) { + if (runtime_combos[i].combo.key_position_len > 0) { + break; + } + + ret++; + } + + return ret; +} + +static void reload_from_static(void) { + memset(runtime_combos, 0, ARRAY_SIZE(runtime_combos) * sizeof(struct zmk_combo_runtime)); + for (int i = 0; i < ARRAY_SIZE(runtime_combos); i++) { + runtime_combos[i].id = i; + if (i < ARRAY_SIZE(combos)) { + memcpy(&runtime_combos[i].combo, &combos[i], sizeof(struct combo_cfg)); + } + } + + reload_combo_lookup(); +} + +bool zmk_combos_check_unsaved_changes(void) { return runtime_combos_changed_ids != 0; } + +#define COMBOS_SETTING_NAME_PREFIX "zmk/combos" + +int zmk_combos_reset_settings(void) { +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE) + // Delete the settings saved for eevery combo ID + for (int i = 0; i < ZMK_COMBOS_LEN; i++) { + char setting_name[14]; + struct zmk_combo_runtime *rc = &runtime_combos[i]; + sprintf(setting_name, COMBOS_SETTING_NAME_PREFIX "/%d", rc->id); + settings_delete(setting_name); + } + +#endif // IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE) + + reload_from_static(); + + return 0; +} + +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE) + +int save_runtime_combo(uint8_t combo_idx) { + char setting_name[14]; + struct zmk_combo_runtime *rc = &runtime_combos[combo_idx]; + struct combo_settings_storage combo_storage = { + .core = + { + .require_prior_idle_ms = rc->combo.require_prior_idle_ms, + .timeout_ms = rc->combo.timeout_ms, + .layer_mask = rc->combo.layer_mask, + .behavior = + { + .behavior_local_id = + zmk_behavior_get_local_id(rc->combo.behavior.behavior_dev), + .param1 = rc->combo.behavior.param1, + .param2 = rc->combo.behavior.param2, + }, + .slow_release = rc->combo.slow_release, + }, + }; + + memcpy(combo_storage.key_positions, rc->combo.key_positions, + MAX_COMBO_KEYS * sizeof(rc->combo.key_positions[0])); + + sprintf(setting_name, COMBOS_SETTING_NAME_PREFIX "/%d", rc->id); + + // Optimize storage a bit by only storing keys that are set + return settings_save_one(setting_name, &combo_storage, + sizeof(struct combo_settings_storage_core) + + rc->combo.key_position_len * sizeof(rc->combo.key_positions[0])); +} + +#endif + +int zmk_combos_save_changes(void) { +#if !IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE) + return -ENOTSUP; +#else + LOG_DBG("Changed? %d", runtime_combos_changed_ids); + for (size_t i = 0; i < ZMK_COMBOS_LEN; i++) { + struct zmk_combo_runtime *rc = &runtime_combos[i]; + LOG_DBG("Checking ID %d", rc->id); + if (!IS_BIT_SET(runtime_combos_changed_ids, rc->id)) { + continue; + } + + LOG_DBG("Saving combo with ID %d", rc->id); + + int ret = save_runtime_combo(i); + if (ret < 0) { + LOG_DBG("Saving combo with id %d failed (%d)", rc->id, ret); + return ret; + } + + WRITE_BIT(runtime_combos_changed_ids, rc->id, false); + } + + return 0; +#endif +} + +int zmk_combos_discard_changes(void) { + int ret = 0; + + reload_from_static(); +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME_SETTINGS_STORAGE) + ret = settings_load_subtree(COMBOS_SETTING_NAME_PREFIX); + if (ret < 0) { + LOG_ERR("Failed to load a subtree %d", ret); + return ret; + } +#endif + + return ret; +} + +static inline const struct combo_cfg *get_combo_with_id(int combo_id) { + return &runtime_combos[combo_id].combo; +} + +static int combos_handle_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { + // Load the combos from the settings + // + int ret; + const char *rem; + + size_t name_len = settings_name_next(name, &rem); + + /* We shouldn't have anything other than the ID for the combo settings */ + if (rem || !name_len) { + return -EINVAL; + } + + char *endptr; + zmk_combo_runtime_id_t combo_id = strtoul(name, &endptr, 10); + if (*endptr != '\0') { + LOG_ERR("Invalid combo ID %s", name); + return -EINVAL; + } + + struct combo_settings_storage stored_combo; + if (len < sizeof(struct combo_settings_storage_core) || len > sizeof(stored_combo)) { + LOG_ERR("Invalid stored combo size of %d", len); + return -EINVAL; + } + + ret = read_cb(cb_arg, &stored_combo, len); + if (ret < 0) { + LOG_ERR("Failed to load combo from settings (%d)", ret); + return ret; + } + + int idx = find_runtime_idx(combo_id); + if (idx < 0) { + LOG_ERR("Invalid combo ID %d", combo_id); + return -ENODEV; + } + + struct zmk_combo_runtime *rc = &runtime_combos[idx]; + + rc->combo.behavior.param1 = stored_combo.core.behavior.param1; + rc->combo.behavior.param2 = stored_combo.core.behavior.param2; + rc->combo.behavior.local_id = stored_combo.core.behavior.behavior_local_id; + rc->combo.slow_release = stored_combo.core.slow_release != 0; + rc->combo.require_prior_idle_ms = stored_combo.core.require_prior_idle_ms; + rc->combo.timeout_ms = stored_combo.core.timeout_ms; + + size_t num_of_positions = + (len - sizeof(struct combo_settings_storage_core)) / sizeof(stored_combo.key_positions[0]); + memcpy(rc->combo.key_positions, stored_combo.key_positions, + num_of_positions * sizeof(stored_combo.key_positions[0])); + rc->combo.key_position_len = num_of_positions; + + return 0; +} + +static int combos_handle_commit(void) { + for (int i = 0; i < ZMK_COMBOS_LEN; i++) { + struct zmk_combo_runtime *rc = &runtime_combos[i]; + + if (rc->combo.key_position_len && rc->combo.behavior.local_id > 0 && + !rc->combo.behavior.behavior_dev) { + rc->combo.behavior.behavior_dev = + zmk_behavior_find_behavior_name_from_local_id(rc->combo.behavior.local_id); + + if (!rc->combo.behavior.behavior_dev) { + LOG_ERR("Failed to finding device for local ID %d after settings load", + rc->combo.behavior.local_id); + return -EINVAL; + } + } + } + + reindex_combos(); + return 0; +} + +SETTINGS_STATIC_HANDLER_DEFINE(combos, COMBOS_SETTING_NAME_PREFIX, NULL, combos_handle_set, + combos_handle_commit, NULL); + +#else + +static inline const struct combo_cfg *get_combo_with_id(int combo_id) { return &combos[combo_id]; } + +#endif + #define COMBO_ONE(n) +1 #define COMBO_CHILDREN_COUNT (0 DT_INST_FOREACH_CHILD(0, COMBO_ONE)) @@ -138,7 +661,7 @@ static void store_last_tapped(int64_t 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(size_t index) { - const struct combo_cfg *new_combo = &combos[index]; + const struct combo_cfg *new_combo = get_combo_with_id(index); for (size_t kp = 0; kp < new_combo->key_position_len; kp++) { sys_bitfield_set_bit((mem_addr_t)&combo_lookup[new_combo->key_positions[kp]], index); @@ -147,6 +670,13 @@ static int initialize_combo(size_t index) { return 0; } +static void reload_combo_lookup(void) { + memset(combo_lookup, 0, ZMK_KEYMAP_LEN * BYTES_FOR_COMBOS_MASK * sizeof(uint32_t)); + for (size_t i = 0; i < ZMK_COMBOS_LEN && get_combo_with_id(i)->key_position_len > 0; i++) { + initialize_combo(i); + } +} + static bool combo_active_on_layer(const struct combo_cfg *combo, uint8_t layer) { if (!combo->layer_mask) { return true; @@ -163,9 +693,11 @@ static int setup_candidates_for_first_keypress(int32_t position, int64_t timesta int number_of_combo_candidates = 0; uint8_t highest_active_layer = zmk_keymap_highest_layer_active(); - for (size_t i = 0; i < ARRAY_SIZE(combos); i++) { - if (sys_bitfield_test_bit((mem_addr_t)&combo_lookup[position], i)) { - const struct combo_cfg *combo = &combos[i]; + for (size_t i = 0; i < ZMK_COMBOS_LEN; i++) { + const struct combo_cfg *combo = get_combo_with_id(i); + if (combo->key_position_len > 1 && + sys_bitfield_test_bit((mem_addr_t)&combo_lookup[position], i)) { + LOG_WRN("Git a matching position at index "); if (combo_active_on_layer(combo, highest_active_layer) && !is_quick_tap(combo, timestamp)) { sys_bitfield_set_bit((mem_addr_t)&candidates, i); @@ -207,9 +739,14 @@ static int64_t first_candidate_timeout() { } int64_t first_timeout = LONG_MAX; - for (int i = 0; i < ARRAY_SIZE(combos); i++) { + for (int i = 0; i < ZMK_COMBOS_LEN; i++) { + const struct combo_cfg *combo = get_combo_with_id(i); + if (combo->key_position_len == 0) { + break; + } + if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) { - first_timeout = MIN(first_timeout, combos[i].timeout_ms); + first_timeout = MIN(first_timeout, combo->timeout_ms); } } @@ -230,11 +767,12 @@ static int cleanup(); static int filter_timed_out_candidates(int64_t timestamp) { __ASSERT(pressed_keys_count > 0, "Searching for a candidate timeout with no keys pressed"); + LOG_WRN("FILTER TIMED OUT!"); int remaining_candidates = 0; - for (int i = 0; i < ARRAY_SIZE(combos); i++) { + for (int i = 0; i < ZMK_COMBOS_LEN; i++) { if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) { - if (pressed_keys[0].data.timestamp + combos[i].timeout_ms > timestamp) { + if (pressed_keys[0].data.timestamp + get_combo_with_id(i)->timeout_ms > timestamp) { remaining_candidates++; } else { sys_bitfield_clear_bit((mem_addr_t)&candidates, i); @@ -251,10 +789,12 @@ static int filter_timed_out_candidates(int64_t timestamp) { static int capture_pressed_key(const struct zmk_position_state_changed *ev) { if (pressed_keys_count == MAX_COMBO_KEYS) { + LOG_WRN("Bubbling!"); return ZMK_EV_EVENT_BUBBLE; } pressed_keys[pressed_keys_count++] = copy_raised_zmk_position_state_changed(ev); + LOG_WRN("Captured they key!"); return ZMK_EV_EVENT_CAPTURED; } @@ -308,7 +848,8 @@ static inline int release_combo_behavior(int combo_idx, const struct combo_cfg * static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) { - int combo_length = MIN(pressed_keys_count, combos[active_combo->combo_idx].key_position_len); + int combo_length = + MIN(pressed_keys_count, get_combo_with_id(active_combo->combo_idx)->key_position_len); for (int i = 0; i < combo_length; i++) { active_combo->key_positions_pressed[i] = pressed_keys[i]; } @@ -344,7 +885,7 @@ static void activate_combo(int combo_idx) { return; } move_pressed_keys_to_active_combo(active_combo); - press_combo_behavior(combo_idx, &combos[combo_idx], + press_combo_behavior(combo_idx, get_combo_with_id(combo_idx), active_combo->key_positions_pressed[0].data.timestamp); } @@ -365,7 +906,7 @@ static bool release_combo_key(int32_t position, int64_t timestamp) { bool key_released = false; bool all_keys_pressed = active_combo->key_positions_pressed_count == - combos[active_combo->combo_idx].key_position_len; + get_combo_with_id(active_combo->combo_idx)->key_position_len; bool all_keys_released = true; for (int i = 0; i < active_combo->key_positions_pressed_count; i++) { if (key_released) { @@ -380,7 +921,7 @@ static bool release_combo_key(int32_t position, int64_t timestamp) { if (key_released) { active_combo->key_positions_pressed_count--; - const struct combo_cfg *c = &combos[active_combo->combo_idx]; + const struct combo_cfg *c = get_combo_with_id(active_combo->combo_idx); if ((c->slow_release && all_keys_released) || (!c->slow_release && all_keys_pressed)) { release_combo_behavior(active_combo->combo_idx, c, timestamp); } @@ -435,9 +976,13 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_ update_timeout_task(); if (num_candidates) { - for (int i = 0; i < ARRAY_SIZE(combos); i++) { + for (int i = 0; i < ZMK_COMBOS_LEN; i++) { + const struct combo_cfg *candidate_combo = get_combo_with_id(i); + if (candidate_combo->key_position_len == 0) { + break; + } + if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) { - const struct combo_cfg *candidate_combo = &combos[i]; if (candidate_is_completely_pressed(candidate_combo)) { fully_pressed_combo = i; if (num_candidates == 1) { @@ -526,10 +1071,14 @@ static int combo_init(void) { } k_work_init_delayable(&timeout_task, combo_timeout_handler); - LOG_WRN("Have %d combos!", ARRAY_SIZE(combos)); + +#if IS_ENABLED(CONFIG_ZMK_COMBOS_RUNTIME) + reload_from_static(); +#else for (int i = 0; i < ARRAY_SIZE(combos); i++) { initialize_combo(i); } +#endif return 0; } diff --git a/app/src/keymap.c b/app/src/keymap.c index 393eb61ec..d3ef95d59 100644 --- a/app/src/keymap.c +++ b/app/src/keymap.c @@ -466,12 +466,6 @@ int zmk_keymap_set_layer_name(zmk_keymap_layer_id_t id, const char *name, size_t static uint8_t zmk_keymap_layer_pending_changes[ZMK_KEYMAP_LAYERS_LEN][PENDING_ARRAY_SIZE]; -struct zmk_behavior_binding_setting { - zmk_behavior_local_id_t behavior_local_id; - uint32_t param1; - uint32_t param2; -} __packed; - bool zmk_keymap_check_unsaved_changes(void) { for (int l = 0; l < ZMK_KEYMAP_LAYERS_LEN; l++) { uint8_t *pending = zmk_keymap_layer_pending_changes[l];