mirror of https://github.com/zmkfirmware/zmk.git
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.
This commit is contained in:
parent
bd2fc145c7
commit
3042c212a9
|
|
@ -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})
|
||||
|
|
|
|||
16
app/Kconfig
16
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -7,10 +7,82 @@
|
|||
#pragma once
|
||||
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zmk/behavior.h>
|
||||
|
||||
#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
|
||||
|
|
|
|||
625
app/src/combo.c
625
app/src/combo.c
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/sys/dlist.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in New Issue