diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 072d5672912..7da5b9eedf4 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -25,7 +25,6 @@ zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/ext_power.h) target_include_directories(app PRIVATE include) target_sources(app PRIVATE src/stdlib.c) target_sources(app PRIVATE src/activity.c) -target_sources(app PRIVATE src/behavior.c) target_sources_ifdef(CONFIG_ZMK_KSCAN_SIDEBAND_BEHAVIORS app PRIVATE src/kscan_sideband_behaviors.c) target_sources(app PRIVATE src/matrix_transform.c) target_sources(app PRIVATE src/physical_layouts.c) @@ -66,7 +65,9 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MOUSE_KEY_PRESS app PRIVATE src/behaviors/behavior_mouse_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_STUDIO_UNLOCK app PRIVATE src/behaviors/behavior_studio_unlock.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_INPUT_TWO_AXIS app PRIVATE src/behaviors/behavior_input_two_axis.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_ARRAY app PRIVATE src/behaviors/behavior_array.c) target_sources(app PRIVATE src/combo.c) + target_sources(app PRIVATE src/behaviors/behavior_combo_trigger.c) target_sources(app PRIVATE src/behaviors/behavior_tap_dance.c) target_sources(app PRIVATE src/behavior_queue.c) target_sources(app PRIVATE src/conditional_layer.c) @@ -74,6 +75,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/events/endpoint_changed.c) target_sources(app PRIVATE src/hid_listener.c) target_sources(app PRIVATE src/keymap.c) + target_sources(app PRIVATE src/events/action_behavior_triggered.c) target_sources(app PRIVATE src/events/layer_state_changed.c) target_sources(app PRIVATE src/events/modifiers_state_changed.c) target_sources(app PRIVATE src/events/keycode_state_changed.c) @@ -87,6 +89,8 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) endif() endif() +target_sources(app PRIVATE src/behavior.c) + target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) diff --git a/app/Kconfig b/app/Kconfig index 971c4991c7c..0e8e6329014 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -438,16 +438,21 @@ endmenu menu "Combo options" +config ZMK_COMBO_MAX_TRIGGER_NUM + int "Upper bound of numbers that can be passed to a combo trigger behavior" + default 20 + config ZMK_COMBO_MAX_PRESSED_COMBOS int "Maximum number of currently pressed combos" default 4 -config ZMK_COMBO_MAX_COMBOS_PER_KEY - int "Maximum number of combos per key" - default 5 +config ZMK_COMBO_MAX_COMBOS_PER_TRIGGER + int "Maximum number of combos per trigger" + default 8 -config ZMK_COMBO_MAX_KEYS_PER_COMBO - int "Maximum number of keys per combo" +config ZMK_COMBO_MAX_TRIGGERS_PER_COMBO + int "Maximum number of triggers per combo" + range 1 63 default 4 # Combo options diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index 5002bcac4b6..815122a81fe 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -122,3 +122,8 @@ config ZMK_BEHAVIOR_MACRO bool default y depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED + +config ZMK_BEHAVIOR_ARRAY + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_ARRAY_ENABLED diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d5c5..4da16427072 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,5 @@ #include #include #include +#include + diff --git a/app/dts/behaviors/combo_kp.dtsi b/app/dts/behaviors/combo_kp.dtsi new file mode 100644 index 00000000000..0c650416bed --- /dev/null +++ b/app/dts/behaviors/combo_kp.dtsi @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { +#if ZMK_BEHAVIOR_OMIT(COMBO_KP) + /omit-if-no-ref/ +#endif + combo_kp: combo_trigger_or_key_press { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or Key Press"; + fallback-behavior = <&kp>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-array.yaml b/app/dts/bindings/behaviors/zmk,behavior-array.yaml new file mode 100644 index 00000000000..cd227a0ccea --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-array.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Array of Behaviors + +compatible: "zmk,behavior-array" + +include: one_param.yaml + +properties: + bindings: + type: phandle-array + required: true diff --git a/app/dts/bindings/behaviors/zmk,behavior-combo-trigger.yaml b/app/dts/bindings/behaviors/zmk,behavior-combo-trigger.yaml new file mode 100644 index 00000000000..2ab74a101f4 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-combo-trigger.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Combo Trigger + +compatible: "zmk,behavior-combo-trigger" + +include: two_param.yaml + +properties: + fallback-behavior: + type: phandle + required: true diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml index f146ab7a230..4efda3991bc 100644 --- a/app/dts/bindings/zmk,combos.yaml +++ b/app/dts/bindings/zmk,combos.yaml @@ -12,7 +12,7 @@ child-binding: bindings: type: phandle-array required: true - key-positions: + triggers: type: array required: true timeout-ms: @@ -23,6 +23,3 @@ child-binding: default: -1 slow-release: type: boolean - layers: - type: array - default: [-1] diff --git a/app/include/drivers/behavior.h b/app/include/drivers/behavior.h index 56c26a0155c..484e81447d7 100644 --- a/app/include/drivers/behavior.h +++ b/app/include/drivers/behavior.h @@ -81,7 +81,10 @@ enum behavior_locality { BEHAVIOR_LOCALITY_GLOBAL }; +enum behavior_type { BEHAVIOR_TYPE_CONTROL_FLOW, BEHAVIOR_TYPE_ACTION }; + __subsystem struct behavior_driver_api { + enum behavior_type type; enum behavior_locality locality; behavior_keymap_binding_callback_t binding_convert_central_state_dependent_params; behavior_keymap_binding_callback_t binding_pressed; @@ -314,6 +317,27 @@ static inline int z_impl_behavior_get_locality(const struct device *behavior, return 0; } +/** + * @brief Determine what type the behavior is + * @param behavior Pointer to the device structure for the driver instance. + * + * @retval Zero if successful. + * @retval Negative errno code if failure. + */ +__syscall int behavior_get_type(const struct device *behavior, enum behavior_type *type); + +static inline int z_impl_behavior_get_type(const struct device *behavior, + enum behavior_type *type) { + if (behavior == NULL) { + return -EINVAL; + } + + const struct behavior_driver_api *api = (const struct behavior_driver_api *)behavior->api; + *type = api->type; + + return 0; +} + /** * @brief Handle the keymap binding being pressed * @param dev Pointer to the device structure for the driver instance. diff --git a/app/include/zmk/combos.h b/app/include/zmk/combos.h index 644a2b53fa8..7d3e0253661 100644 --- a/app/include/zmk/combos.h +++ b/app/include/zmk/combos.h @@ -14,3 +14,7 @@ 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)) + +int zmk_combo_trigger_behavior_invoked(int trigger_id, char *fallback_behavior_dev, + uint32_t fallback_param, + struct zmk_behavior_binding_event event, bool state); \ No newline at end of file diff --git a/app/include/zmk/events/action_behavior_triggered.h b/app/include/zmk/events/action_behavior_triggered.h new file mode 100644 index 00000000000..6d263be56f5 --- /dev/null +++ b/app/include/zmk/events/action_behavior_triggered.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_action_behavior_triggered { + const struct zmk_behavior_binding *binding; + struct zmk_behavior_binding_event event; + bool pressed; +}; + +ZMK_EVENT_DECLARE(zmk_action_behavior_triggered); + +static inline int raise_action_behavior_triggered(const struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event, + bool pressed) { + return raise_zmk_action_behavior_triggered((struct zmk_action_behavior_triggered){ + .binding = binding, .event = event, .pressed = pressed}); +} \ No newline at end of file diff --git a/app/src/behavior.c b/app/src/behavior.c index 9b20c706265..f9bae496f64 100644 --- a/app/src/behavior.c +++ b/app/src/behavior.c @@ -28,6 +28,7 @@ #include #include +#include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -65,8 +66,8 @@ static int invoke_locally(struct zmk_behavior_binding *binding, } } -int zmk_behavior_invoke_binding(const struct zmk_behavior_binding *src_binding, - struct zmk_behavior_binding_event event, bool pressed) { +static int invoke_binding(const struct zmk_behavior_binding *src_binding, + struct zmk_behavior_binding_event event, bool pressed) { // We want to make a copy of this, since it may be converted from // relative to absolute before being invoked struct zmk_behavior_binding binding = *src_binding; @@ -116,6 +117,43 @@ int zmk_behavior_invoke_binding(const struct zmk_behavior_binding *src_binding, return -ENOTSUP; } +int zmk_behavior_invoke_binding(const struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event, bool pressed) { + const struct device *behavior = zmk_behavior_get_binding(binding->behavior_dev); + + if (!behavior) { + LOG_WRN("No behavior assigned to %d on layer %d", event.position, event.layer); + return 1; + } + + enum behavior_type type = BEHAVIOR_TYPE_ACTION; + int err = behavior_get_type(behavior, &type); + if (err) { + LOG_ERR("Failed to get behavior type %d", err); + return err; + } + if (type == BEHAVIOR_TYPE_ACTION) { + err = raise_action_behavior_triggered(binding, event, pressed); + if (err) { + LOG_ERR("Failed to raise action behavior event %d", err); + return err; + } + return ZMK_BEHAVIOR_OPAQUE; + } + return invoke_binding(binding, event, pressed); +} + +int behavior_action_behavior_triggered_listener(const zmk_event_t *eh) { + struct zmk_action_behavior_triggered *ev = as_zmk_action_behavior_triggered(eh); + if (ev != NULL) { + return invoke_binding(ev->binding, ev->event, ev->pressed); + } + return ZMK_EV_EVENT_BUBBLE; +} + +ZMK_LISTENER(behavior_action, behavior_action_behavior_triggered_listener); +ZMK_SUBSCRIPTION(behavior_action, zmk_action_behavior_triggered); + #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) int zmk_behavior_get_empty_param_metadata(const struct device *dev, diff --git a/app/src/behaviors/behavior_array.c b/app/src/behaviors/behavior_array.c new file mode 100644 index 00000000000..7ccd3f1854f --- /dev/null +++ b/app/src/behaviors/behavior_array.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_array + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct behavior_array_config { + size_t behavior_count; + struct zmk_behavior_binding *behaviors; +}; + +static int on_array_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + const struct behavior_array_config *cfg = dev->config; + int index = binding->param1; + + if (index >= cfg->behavior_count || index < 0) { + LOG_ERR("Trying to trigger an index beyond the size of the behavior array."); + return -ENOTSUP; + } + return zmk_behavior_invoke_binding((struct zmk_behavior_binding *)&cfg->behaviors[index], event, + true); +} + +static int on_array_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + const struct behavior_array_config *cfg = dev->config; + int index = binding->param1; + + if (index >= cfg->behavior_count || index < 0) { + LOG_ERR("Trying to trigger an index beyond the size of the behavior array."); + return -ENOTSUP; + } + return zmk_behavior_invoke_binding((struct zmk_behavior_binding *)&cfg->behaviors[index], event, + false); +} + +static const struct behavior_driver_api behavior_array_driver_api = { + .binding_pressed = on_array_binding_pressed, + .binding_released = on_array_binding_released, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int behavior_array_init(const struct device *dev) { return 0; } + +#define _TRANSFORM_ENTRY(idx, node) ZMK_KEYMAP_EXTRACT_BINDING(idx, node) + +#define TRANSFORMED_BINDINGS(node) \ + {LISTIFY(DT_INST_PROP_LEN(node, bindings), _TRANSFORM_ENTRY, (, ), DT_DRV_INST(node))} + +#define ARR_INST(n) \ + static struct zmk_behavior_binding \ + behavior_array_config_##n##_bindings[DT_INST_PROP_LEN(n, bindings)] = \ + TRANSFORMED_BINDINGS(n); \ + static struct behavior_array_config behavior_array_config_##n = { \ + .behaviors = behavior_array_config_##n##_bindings, \ + .behavior_count = DT_INST_PROP_LEN(n, bindings)}; \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_array_init, NULL, NULL, &behavior_array_config_##n, \ + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_array_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(ARR_INST) + +#endif diff --git a/app/src/behaviors/behavior_backlight.c b/app/src/behaviors/behavior_backlight.c index 45edd4c923e..f5049c9b73b 100644 --- a/app/src/behaviors/behavior_backlight.c +++ b/app/src/behaviors/behavior_backlight.c @@ -165,6 +165,7 @@ static const struct behavior_driver_api behavior_backlight_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, .locality = BEHAVIOR_LOCALITY_GLOBAL, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif diff --git a/app/src/behaviors/behavior_bt.c b/app/src/behaviors/behavior_bt.c index f439e49b1cf..e0c974515c8 100644 --- a/app/src/behaviors/behavior_bt.c +++ b/app/src/behaviors/behavior_bt.c @@ -122,6 +122,7 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_bt_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_caps_word.c b/app/src/behaviors/behavior_caps_word.c index c3255f12c15..2e9983c157a 100644 --- a/app/src/behaviors/behavior_caps_word.c +++ b/app/src/behaviors/behavior_caps_word.c @@ -75,6 +75,7 @@ static int on_caps_word_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_caps_word_driver_api = { .binding_pressed = on_caps_word_binding_pressed, .binding_released = on_caps_word_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_combo_trigger.c b/app/src/behaviors/behavior_combo_trigger.c new file mode 100644 index 00000000000..3b835d21082 --- /dev/null +++ b/app/src/behaviors/behavior_combo_trigger.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_combo_trigger + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct behavior_combo_trigger_config { + char *fallback_behavior_dev; +}; + +static int on_combo_trigger_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct behavior_combo_trigger_config *cfg = + zmk_behavior_get_binding(binding->behavior_dev)->config; + return zmk_combo_trigger_behavior_invoked(binding->param1, cfg->fallback_behavior_dev, + binding->param2, event, true); +} + +static int on_combo_trigger_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct behavior_combo_trigger_config *cfg = + zmk_behavior_get_binding(binding->behavior_dev)->config; + return zmk_combo_trigger_behavior_invoked(binding->param1, cfg->fallback_behavior_dev, + binding->param2, event, false); +} + +static const struct behavior_driver_api behavior_combo_trigger_driver_api = { + .binding_pressed = on_combo_trigger_binding_pressed, + .binding_released = on_combo_trigger_binding_released, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int behavior_combo_trigger_init(const struct device *dev) { return 0; } + +#define CT_INST(n) \ + static struct behavior_combo_trigger_config behavior_combo_trigger_config_##n = { \ + .fallback_behavior_dev = DEVICE_DT_NAME(DT_INST_PHANDLE(n, fallback_behavior)), \ + }; \ + BEHAVIOR_DT_INST_DEFINE( \ + n, behavior_combo_trigger_init, NULL, NULL, &behavior_combo_trigger_config_##n, \ + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_combo_trigger_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(CT_INST) + +#endif diff --git a/app/src/behaviors/behavior_ext_power.c b/app/src/behaviors/behavior_ext_power.c index b2aff3c8670..1f2f200a432 100644 --- a/app/src/behaviors/behavior_ext_power.c +++ b/app/src/behaviors/behavior_ext_power.c @@ -71,6 +71,7 @@ static const struct behavior_driver_api behavior_ext_power_driver_api = { on_keymap_binding_convert_central_state_dependent_params, .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, .locality = BEHAVIOR_LOCALITY_GLOBAL, }; diff --git a/app/src/behaviors/behavior_input_two_axis.c b/app/src/behaviors/behavior_input_two_axis.c index ea8e948d21c..e73a2666176 100644 --- a/app/src/behaviors/behavior_input_two_axis.c +++ b/app/src/behaviors/behavior_input_two_axis.c @@ -284,7 +284,10 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, } static const struct behavior_driver_api behavior_input_two_axis_driver_api = { - .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, +}; #define ITA_INST(n) \ static struct behavior_input_two_axis_data behavior_input_two_axis_data_##n = {}; \ diff --git a/app/src/behaviors/behavior_key_press.c b/app/src/behaviors/behavior_key_press.c index b090401ec50..1c80e7b94f3 100644 --- a/app/src/behaviors/behavior_key_press.c +++ b/app/src/behaviors/behavior_key_press.c @@ -54,6 +54,7 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_key_press_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_key_repeat.c b/app/src/behaviors/behavior_key_repeat.c index 21343ae8142..192d6d121cb 100644 --- a/app/src/behaviors/behavior_key_repeat.c +++ b/app/src/behaviors/behavior_key_repeat.c @@ -67,6 +67,7 @@ static int on_key_repeat_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_key_repeat_driver_api = { .binding_pressed = on_key_repeat_binding_pressed, .binding_released = on_key_repeat_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_key_toggle.c b/app/src/behaviors/behavior_key_toggle.c index 355f4e465de..292d6e0a146 100644 --- a/app/src/behaviors/behavior_key_toggle.c +++ b/app/src/behaviors/behavior_key_toggle.c @@ -78,6 +78,7 @@ static const struct behavior_parameter_metadata metadata = { static const struct behavior_driver_api behavior_key_toggle_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_momentary_layer.c b/app/src/behaviors/behavior_momentary_layer.c index b781a953771..dc7c436c193 100644 --- a/app/src/behaviors/behavior_momentary_layer.c +++ b/app/src/behaviors/behavior_momentary_layer.c @@ -56,6 +56,7 @@ static int mo_keymap_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_mo_driver_api = { .binding_pressed = mo_keymap_binding_pressed, .binding_released = mo_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_mouse_key_press.c b/app/src/behaviors/behavior_mouse_key_press.c index 66b54fce05b..956deaf2212 100644 --- a/app/src/behaviors/behavior_mouse_key_press.c +++ b/app/src/behaviors/behavior_mouse_key_press.c @@ -49,7 +49,10 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, } static const struct behavior_driver_api behavior_mouse_key_press_driver_api = { - .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, +}; #define MKP_INST(n) \ BEHAVIOR_DT_INST_DEFINE(n, behavior_mouse_key_press_init, NULL, NULL, NULL, POST_KERNEL, \ diff --git a/app/src/behaviors/behavior_none.c b/app/src/behaviors/behavior_none.c index b1dc4ad3327..e8ddfa18693 100644 --- a/app/src/behaviors/behavior_none.c +++ b/app/src/behaviors/behavior_none.c @@ -31,6 +31,7 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_none_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_outputs.c b/app/src/behaviors/behavior_outputs.c index ffa57d16357..fec1250394b 100644 --- a/app/src/behaviors/behavior_outputs.c +++ b/app/src/behaviors/behavior_outputs.c @@ -76,6 +76,7 @@ static int behavior_out_init(const struct device *dev) { return 0; } static const struct behavior_driver_api behavior_outputs_driver_api = { .binding_pressed = on_keymap_binding_pressed, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_reset.c b/app/src/behaviors/behavior_reset.c index 554132f4a1e..9e35047af2c 100644 --- a/app/src/behaviors/behavior_reset.c +++ b/app/src/behaviors/behavior_reset.c @@ -38,6 +38,7 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_reset_driver_api = { .binding_pressed = on_keymap_binding_pressed, .locality = BEHAVIOR_LOCALITY_EVENT_SOURCE, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_rgb_underglow.c b/app/src/behaviors/behavior_rgb_underglow.c index c37e5217c73..fd0f4d58fbf 100644 --- a/app/src/behaviors/behavior_rgb_underglow.c +++ b/app/src/behaviors/behavior_rgb_underglow.c @@ -260,6 +260,7 @@ static const struct behavior_driver_api behavior_rgb_underglow_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, .locality = BEHAVIOR_LOCALITY_GLOBAL, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif diff --git a/app/src/behaviors/behavior_soft_off.c b/app/src/behaviors/behavior_soft_off.c index fcffd09ae5e..ca73e1cde0d 100644 --- a/app/src/behaviors/behavior_soft_off.c +++ b/app/src/behaviors/behavior_soft_off.c @@ -74,6 +74,7 @@ static const struct behavior_driver_api behavior_soft_off_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, .locality = BEHAVIOR_LOCALITY_GLOBAL, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_studio_unlock.c b/app/src/behaviors/behavior_studio_unlock.c index 95f2b40d176..b9a9284b853 100644 --- a/app/src/behaviors/behavior_studio_unlock.c +++ b/app/src/behaviors/behavior_studio_unlock.c @@ -34,6 +34,7 @@ static int on_keymap_binding_released(struct zmk_behavior_binding *binding, static const struct behavior_driver_api behavior_studio_unlock_driver_api = { .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_tap_dance.c b/app/src/behaviors/behavior_tap_dance.c index 5423f93f7dc..ef746a4c269 100644 --- a/app/src/behaviors/behavior_tap_dance.c +++ b/app/src/behaviors/behavior_tap_dance.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,10 @@ struct behavior_tap_dance_config { struct active_tap_dance { // Tap Dance Data int counter; - uint32_t position; + // First position is the "proper" position. + // All others are to track tap-dances inside of combos correctly + uint32_t positions[CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO + 1]; + uint8_t positions_len; #if IS_ENABLED(CONFIG_ZMK_SPLIT) uint8_t source; #endif @@ -51,11 +55,24 @@ struct active_tap_dance { struct k_work_delayable release_timer; }; +uint32_t potential_positions[CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO] = {}; +uint8_t potential_positions_len = 0; + +static inline int add_to_potential_positions(uint32_t position) { + if (potential_positions_len == CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO) { + LOG_ERR("tap dance: Failed to store potential tap dance position %d", position); + return -ENOMEM; + } + potential_positions[potential_positions_len++] = position; + return 0; +} + struct active_tap_dance active_tap_dances[ZMK_BHV_TAP_DANCE_MAX_HELD] = {}; static struct active_tap_dance *find_tap_dance(uint32_t position) { for (int i = 0; i < ZMK_BHV_TAP_DANCE_MAX_HELD; i++) { - if (active_tap_dances[i].position == position && !active_tap_dances[i].timer_cancelled) { + if (active_tap_dances[i].positions[0] == position && + !active_tap_dances[i].timer_cancelled) { return &active_tap_dances[i]; } } @@ -65,11 +82,12 @@ static struct active_tap_dance *find_tap_dance(uint32_t position) { static int new_tap_dance(struct zmk_behavior_binding_event *event, const struct behavior_tap_dance_config *config, struct active_tap_dance **tap_dance) { + for (int i = 0; i < ZMK_BHV_TAP_DANCE_MAX_HELD; i++) { struct active_tap_dance *const ref_dance = &active_tap_dances[i]; - if (ref_dance->position == ZMK_BHV_TAP_DANCE_POSITION_FREE) { + if (ref_dance->positions[0] == ZMK_BHV_TAP_DANCE_POSITION_FREE) { ref_dance->counter = 0; - ref_dance->position = event->position; + ref_dance->positions[0] = event->position; #if IS_ENABLED(CONFIG_ZMK_SPLIT) ref_dance->source = event->source; #endif @@ -79,6 +97,11 @@ static int new_tap_dance(struct zmk_behavior_binding_event *event, ref_dance->timer_started = true; ref_dance->timer_cancelled = false; ref_dance->tap_dance_decided = false; + for (int i = 1; i < potential_positions_len + 1; i++) { + ref_dance->positions[i] = potential_positions[i]; + } + ref_dance->positions_len = 1 + potential_positions_len; + potential_positions_len = 0; *tap_dance = ref_dance; return 0; } @@ -87,7 +110,7 @@ static int new_tap_dance(struct zmk_behavior_binding_event *event, } static void clear_tap_dance(struct active_tap_dance *tap_dance) { - tap_dance->position = ZMK_BHV_TAP_DANCE_POSITION_FREE; + tap_dance->positions[0] = ZMK_BHV_TAP_DANCE_POSITION_FREE; } static int stop_timer(struct active_tap_dance *tap_dance) { @@ -105,7 +128,7 @@ static void reset_timer(struct active_tap_dance *tap_dance, int32_t ms_left = tap_dance->release_at - k_uptime_get(); if (ms_left > 0) { k_work_schedule(&tap_dance->release_timer, K_MSEC(ms_left)); - LOG_DBG("Successfully reset timer at position %d", tap_dance->position); + LOG_DBG("Successfully reset timer at position %d", tap_dance->positions[0]); } } @@ -113,7 +136,7 @@ static inline int press_tap_dance_behavior(struct active_tap_dance *tap_dance, i tap_dance->tap_dance_decided = true; struct zmk_behavior_binding binding = tap_dance->config->behaviors[tap_dance->counter - 1]; struct zmk_behavior_binding_event event = { - .position = tap_dance->position, + .position = tap_dance->positions[0], .timestamp = timestamp, #if IS_ENABLED(CONFIG_ZMK_SPLIT) .source = tap_dance->source, @@ -126,7 +149,7 @@ static inline int release_tap_dance_behavior(struct active_tap_dance *tap_dance, int64_t timestamp) { struct zmk_behavior_binding binding = tap_dance->config->behaviors[tap_dance->counter - 1]; struct zmk_behavior_binding_event event = { - .position = tap_dance->position, + .position = tap_dance->positions[0], .timestamp = timestamp, #if IS_ENABLED(CONFIG_ZMK_SPLIT) .source = tap_dance->source, @@ -185,7 +208,7 @@ void behavior_tap_dance_timer_handler(struct k_work *item) { struct k_work_delayable *d_work = k_work_delayable_from_work(item); struct active_tap_dance *tap_dance = CONTAINER_OF(d_work, struct active_tap_dance, release_timer); - if (tap_dance->position == ZMK_BHV_TAP_DANCE_POSITION_FREE) { + if (tap_dance->positions[0] == ZMK_BHV_TAP_DANCE_POSITION_FREE) { return; } if (tap_dance->timer_cancelled) { @@ -207,12 +230,23 @@ static const struct behavior_driver_api behavior_tap_dance_driver_api = { #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) }; -static int tap_dance_position_state_changed_listener(const zmk_event_t *eh); +static int position_state_changed_listener(const zmk_event_t *eh); +static int action_behavior_triggered_listener(const zmk_event_t *eh); + +int behavior_tap_dance_listener(const zmk_event_t *eh) { + if (as_zmk_action_behavior_triggered(eh) != NULL) { + return action_behavior_triggered_listener(eh); + } else if (as_zmk_position_state_changed(eh) != NULL) { + return position_state_changed_listener(eh); + } + return ZMK_EV_EVENT_BUBBLE; +} -ZMK_LISTENER(behavior_tap_dance, tap_dance_position_state_changed_listener); +ZMK_LISTENER(behavior_tap_dance, behavior_tap_dance_listener); +ZMK_SUBSCRIPTION(behavior_tap_dance, zmk_action_behavior_triggered); ZMK_SUBSCRIPTION(behavior_tap_dance, zmk_position_state_changed); -static int tap_dance_position_state_changed_listener(const zmk_event_t *eh) { +static int position_state_changed_listener(const zmk_event_t *eh) { struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); if (ev == NULL) { return ZMK_EV_EVENT_BUBBLE; @@ -223,22 +257,35 @@ static int tap_dance_position_state_changed_listener(const zmk_event_t *eh) { } for (int i = 0; i < ZMK_BHV_TAP_DANCE_MAX_HELD; i++) { struct active_tap_dance *tap_dance = &active_tap_dances[i]; - if (tap_dance->position == ZMK_BHV_TAP_DANCE_POSITION_FREE) { + bool tap_dance_at_position = false; + if (tap_dance->positions[0] == ZMK_BHV_TAP_DANCE_POSITION_FREE) { continue; } - if (tap_dance->position == ev->position) { - continue; + // Need to check the whole positions array in case tap dance in a combo + for (int j = 0; j < tap_dance->positions_len; j++) { + if (tap_dance->positions[j] == ev->position) { + tap_dance_at_position = true; + break; + } } - stop_timer(tap_dance); - LOG_DBG("Tap dance interrupted, activating tap-dance at %d", tap_dance->position); - if (!tap_dance->tap_dance_decided) { - press_tap_dance_behavior(tap_dance, ev->timestamp); - if (!tap_dance->is_pressed) { - release_tap_dance_behavior(tap_dance, ev->timestamp); + if (!tap_dance_at_position) { + stop_timer(tap_dance); + LOG_DBG("Tap dance interrupted, activating tap-dance at %d", tap_dance->positions[0]); + if (!tap_dance->tap_dance_decided) { + press_tap_dance_behavior(tap_dance, ev->timestamp); + if (!tap_dance->is_pressed) { + release_tap_dance_behavior(tap_dance, ev->timestamp); + } + return ZMK_EV_EVENT_BUBBLE; } - return ZMK_EV_EVENT_BUBBLE; } } + add_to_potential_positions(ev->position); + return ZMK_EV_EVENT_BUBBLE; +} + +static int action_behavior_triggered_listener(const zmk_event_t *eh) { + potential_positions_len = 0; return ZMK_EV_EVENT_BUBBLE; } diff --git a/app/src/behaviors/behavior_to_layer.c b/app/src/behaviors/behavior_to_layer.c index f739ec8debf..91b5bf4ff37 100644 --- a/app/src/behaviors/behavior_to_layer.c +++ b/app/src/behaviors/behavior_to_layer.c @@ -56,6 +56,7 @@ static const struct behavior_parameter_metadata metadata = { static const struct behavior_driver_api behavior_to_driver_api = { .binding_pressed = to_keymap_binding_pressed, .binding_released = to_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/behaviors/behavior_toggle_layer.c b/app/src/behaviors/behavior_toggle_layer.c index c804f5844c3..7449e2b6dec 100644 --- a/app/src/behaviors/behavior_toggle_layer.c +++ b/app/src/behaviors/behavior_toggle_layer.c @@ -76,6 +76,7 @@ static const struct behavior_parameter_metadata metadata = { static const struct behavior_driver_api behavior_tog_driver_api = { .binding_pressed = tog_keymap_binding_pressed, .binding_released = tog_keymap_binding_released, + .type = BEHAVIOR_TYPE_ACTION, #if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) .parameter_metadata = &metadata, #endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) diff --git a/app/src/combo.c b/app/src/combo.c index c3334bdb754..918f6a7d494 100644 --- a/app/src/combo.c +++ b/app/src/combo.c @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -27,56 +28,92 @@ 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; + // Mandatory DT props + int32_t *triggered_by; // DT 'triggers' array + uint8_t triggered_by_len; + + struct zmk_behavior_binding behavior; // trigger on successful combo 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; + + // Additional data + // 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[]; + + // Optional DT props + int32_t require_prior_idle_ms; + bool slow_release; // release combo when (T:all, F:one) trigger released }; 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]; +// Marks which of the triggers in the combo are currently active +// used for slow_release and to prevent combo re-entry before all triggers released +// Bit flags corresponding to locations in the 'triggered_by' array +// (1<trigger loop when activating a behavior +bool combo_is_being_activated = false; +// This event exists to allow hold-tap interrupts etc. to work +// It gets raised when combo invokes a behavior, and resolves when it reaches combo again +// Start with negative timestamp to avoid issues with key_position 0 being pressed at time 0 +struct zmk_position_state_changed combo_position_changed_notification = { + .timestamp = -1, +}; + +// used for 'timeout-ms' struct k_work_delayable timeout_task; int64_t timeout_task_timeout_at; +//--- +/* + * Used for 'require-prior-idle-ms' + */ // 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 @@ -87,87 +124,76 @@ static void store_last_tapped(int64_t timestamp) { last_tapped_timestamp = timestamp; } } +//--- -// Store the combo key pointer in the combos array, one pointer for each key position +// Store the combo key pointer in the combo lookup array, one pointer for each trigger // 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); + for (int i = 0; i < new_combo->triggered_by_len; i++) { + int32_t trigger_id = new_combo->triggered_by[i]; + if (trigger_id >= CONFIG_ZMK_COMBO_MAX_TRIGGER_NUM) { + LOG_ERR("Unable to initialize combo, trigger %d does not exist", trigger_id); return -EINVAL; } + // insort the combo 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]; + for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; j++) { + struct combo_cfg *combo_at_j = combo_lookup[trigger_id][j]; if (combo_at_j == NULL) { - combo_lookup[position][j] = insert_combo; + combo_lookup[trigger_id][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 && + if (combo_at_j->triggered_by_len < insert_combo->triggered_by_len || + (combo_at_j->triggered_by_len == insert_combo->triggered_by_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; + combo_lookup[trigger_id][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); + LOG_ERR("Too many combos for trigger %d, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER %d.", + trigger_id, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER); 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; -} - +// Used for 'require-prior-idle-ms' 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) { +// candidates will be sorted as combo_lookup is sorted +static int identify_initial_candidates(int32_t trigger_id, 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]; + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; i++) { + struct combo_cfg *combo = combo_lookup[trigger_id][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; + if (!is_quick_tap(combo, timestamp)) { + candidates[number_of_combo_candidates] = combo; 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) { +static int filter_candidates(int32_t trigger_id) { // 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 + // assuming they are both sorted on triggered_by_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]; + while (lookup_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER && + candidate_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER) { + struct combo_cfg *candidate = candidates[candidate_idx]; + struct combo_cfg *lookup = combo_lookup[trigger_id][lookup_idx]; if (candidate == NULL || lookup == NULL) { break; } @@ -176,9 +202,9 @@ static int filter_candidates(int32_t position) { matches++; candidate_idx++; lookup_idx++; - } else if (candidate->key_position_len > lookup->key_position_len) { + } else if (candidate->triggered_by_len > lookup->triggered_by_len) { lookup_idx++; - } else if (candidate->key_position_len < lookup->key_position_len) { + } else if (candidate->triggered_by_len < lookup->triggered_by_len) { candidate_idx++; } else if (candidate->virtual_key_position > lookup->virtual_key_position) { lookup_idx++; @@ -187,8 +213,8 @@ static int filter_candidates(int32_t position) { } } // clear unmatched candidates - for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { - candidates[i].combo = NULL; + for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; i++) { + candidates[i] = NULL; } // LOG_DBG("combo matches after filter %d", matches); return matches; @@ -196,49 +222,48 @@ static int filter_candidates(int32_t position) { 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) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; i++) { + if (candidates[i] == NULL) { break; } - if (candidates[i].timeout_at < first_timeout) { - first_timeout = candidates[i].timeout_at; + int64_t candidate_timeout = candidate_timestamp + candidates[i]->timeout_ms; + if (candidate_timeout < first_timeout) { + first_timeout = candidate_timeout; } } 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; + /** + * This code assumes that set(active_triggers) <= set(candidate->triggered_by). + * This invariant is enforced by filter_candidates + * Note that a completely pressed candidate is removed from candidates, + * so the case where active_triggers has more elements than triggered_by cannot exist + * If this changes in the future, change the equality to a <= + */ + return candidate->triggered_by_len == stored_triggers_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) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; i++) { + struct combo_cfg *candidate = candidates[i]; + if (candidate == NULL) { break; } - if (candidate->timeout_at > timestamp) { + if (candidate_timestamp + candidate->timeout_ms > 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; + candidates[remaining_candidates] = candidate; // clear the previous location - candidates[i].combo = NULL; - candidates[i].timeout_at = 0; + candidates[i] = NULL; } remaining_candidates++; } else { - candidate->combo = NULL; + candidates[i] = NULL; } } @@ -249,46 +274,82 @@ static int filter_timed_out_candidates(int64_t 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) { +static inline int clear_candidates() { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; i++) { + if (candidates[i] == NULL) { return i; } - candidates[i].combo = NULL; + candidates[i] = NULL; } - return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; + return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER; } -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; - } +static inline int raise_combo_state_changed(bool state, int position, int timestamp) { + struct zmk_position_state_changed notification_event = { + .state = state, + .position = position, + .timestamp = timestamp, +#if IS_ENABLED(CONFIG_ZMK_SPLIT) + .source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL, +#endif + }; + LOG_DBG("combo: raising position state %d", position); + combo_position_changed_notification = notification_event; + return raise_zmk_position_state_changed(notification_event); +} - pressed_keys[pressed_keys_count++] = copy_raised_zmk_position_state_changed(ev); - return ZMK_EV_EVENT_CAPTURED; +// needs to trigger a pos event +static inline int press_fallback_behavior(struct trigger *trigger) { + struct zmk_behavior_binding binding = {.behavior_dev = trigger->fallback_behavior_dev, + .param1 = trigger->fallback_param}; + + raise_combo_state_changed(true, trigger->event.position, trigger->event.timestamp); + return zmk_behavior_invoke_binding(&binding, trigger->event, true); } -const struct zmk_listener zmk_listener_combo; +// needs to trigger a pos event +static inline int release_fallback_behavior(struct trigger *trigger) { + struct zmk_behavior_binding binding = {.behavior_dev = trigger->fallback_behavior_dev, + .param1 = trigger->fallback_param}; + raise_combo_state_changed(false, trigger->event.position, trigger->event.timestamp); + return zmk_behavior_invoke_binding(&binding, trigger->event, false); +} -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]; +// needs to trigger a pos event on failure +static inline int capture_trigger(struct trigger *trigger) { + if (stored_triggers_count == CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER) { + return press_fallback_behavior(trigger); + } + stored_triggers[stored_triggers_count++] = *trigger; + return ZMK_BEHAVIOR_OPAQUE; +} + +// Declare this early, to allow reuse in the below function +static int trigger_state_down(int trigger_id, int64_t timestamp, struct trigger *trigger); + +static int release_stored_triggers(int stored_triggers_offset) { + // Offset allows the reprocessing of superfluous triggers on combo activation + uint32_t count = stored_triggers_offset + stored_triggers_count; + stored_triggers_count = 0; + for (int i = stored_triggers_offset; i < count; i++) { + struct trigger trigger = stored_triggers[i]; if (i == 0) { - LOG_DBG("combo: releasing position event %d", ev->data.position); - ZMK_EVENT_RELEASE(*ev); + LOG_DBG("combo: activating fallback behavior from trigger %d", trigger.trigger_id); + press_fallback_behavior(&trigger); } 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); + // reprocess trigger + //(see tests/combo/fully-overlapping-combos-3 for why this is needed) + LOG_DBG("combo: reprocessing trigger %d", trigger.trigger_id); + trigger_state_down(trigger.trigger_id, trigger.event.timestamp, &stored_triggers[i]); } } - - return count; + return count - stored_triggers_offset; } +// needs to trigger a pos event static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { + last_combo_timestamp = timestamp; + struct zmk_behavior_binding_event event = { .position = combo->virtual_key_position, .timestamp = timestamp, @@ -296,13 +357,13 @@ static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestam .source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL, #endif }; - - last_combo_timestamp = timestamp; - + raise_combo_state_changed(true, combo->virtual_key_position, k_uptime_get()); return zmk_behavior_invoke_binding(&combo->behavior, event, true); } +// needs to trigger a pos event 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, @@ -311,25 +372,10 @@ static inline int release_combo_behavior(struct combo_cfg *combo, int32_t timest #endif }; + raise_combo_state_changed(false, combo->virtual_key_position, k_uptime_get()); 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) { @@ -344,17 +390,23 @@ static struct active_combo *store_active_combo(struct combo_cfg *combo) { return NULL; } -static void activate_combo(struct combo_cfg *combo) { +static int activate_combo(struct combo_cfg *combo) { + // returns the number of triggers used to activate the 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); + return 0; + } + int combo_count = active_combo->combo->triggered_by_len; + active_combo->active_triggers = (1 << combo_count) - 1; + stored_triggers_count -= combo_count; + combo_is_being_activated = true; + press_combo_behavior(combo, candidate_timestamp); + combo_is_being_activated = false; + return combo_count; } +// Removes combo from active_combos array +// keeps active_combos contiguous from 0 static void deactivate_combo(int active_combo_index) { active_combo_count--; if (active_combo_index != active_combo_count) { @@ -362,36 +414,38 @@ static void deactivate_combo(int active_combo_index) { sizeof(struct active_combo)); } active_combos[active_combo_count].combo = NULL; - active_combos[active_combo_count] = (struct active_combo){0}; + // No need to overwrite active_triggers, it will get overwritten when a new combo is activated } -/* returns true if a key was released. */ -static bool release_combo_key(int32_t position, int64_t timestamp) { +/* returns true if a key was released. +releases combo, specific approach depends on slow_release +Each trigger can only activate one combo at once TODO +deactivates combo if all keys were released +*/ +static bool release_combo_key(int32_t trigger_id, 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; + bool trigger_was_released = false; + bool first_released_trigger = + active_combo->active_triggers == (1 << active_combo->combo->triggered_by_len) - 1; + + for (int i = 0; i < active_combo->combo->triggered_by_len; i++) { + if (active_combo->combo->triggered_by[i] == trigger_id) { + active_combo->active_triggers &= ~(1 << i); + trigger_was_released = true; + break; } } + bool last_released_trigger = active_combo->active_triggers == 0; - 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)) { + if (trigger_was_released) { + if ((active_combo->combo->slow_release && last_released_trigger) || + (!active_combo->combo->slow_release && first_released_trigger)) { release_combo_behavior(active_combo->combo, timestamp); } - if (all_keys_released) { + if (last_released_trigger) { + // deactivate only when all triggers are released deactivate_combo(combo_idx); } return true; @@ -400,14 +454,18 @@ static bool release_combo_key(int32_t position, int64_t timestamp) { return false; } -static int cleanup() { +static int combo_cleanup() { + if (combo_is_being_activated) { + return 0; + } k_work_cancel_delayable(&timeout_task); - clear_candidates(); + int offset = 0; if (fully_pressed_combo != NULL) { - activate_combo(fully_pressed_combo); + offset = activate_combo(fully_pressed_combo); fully_pressed_combo = NULL; } - return release_pressed_keys(); + clear_candidates(); + return release_stored_triggers(offset); } static void update_timeout_task() { @@ -425,30 +483,59 @@ static void update_timeout_task() { } } -static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_changed *data) { +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 there are no remaining candidates + if (filter_timed_out_candidates(timeout_task_timeout_at) == 0) { + combo_cleanup(); + } + update_timeout_task(); +} + +// checks if a behavior trigger has already been stored +static bool is_valid_trigger(struct trigger *trigger) { + for (int i = 0; i < stored_triggers_count; i++) { + if (trigger->trigger_id == stored_triggers[i].trigger_id) { + return false; + } + } + return true; +} + +static int trigger_state_down(int trigger_id, int64_t timestamp, struct trigger *trigger) { int num_candidates; - if (candidates[0].combo == NULL) { - num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp); + if (candidates[0] == NULL) { + num_candidates = identify_initial_candidates(trigger_id, timestamp); if (num_candidates == 0) { - return ZMK_EV_EVENT_BUBBLE; + return press_fallback_behavior(trigger); } + candidate_timestamp = timestamp; } else { - filter_timed_out_candidates(data->timestamp); - num_candidates = filter_candidates(data->position); + if (!is_valid_trigger(trigger)) { + LOG_DBG("combo: invalid trigger %d", trigger->trigger_id); + combo_cleanup(); + return press_fallback_behavior(trigger); + } + filter_timed_out_candidates(timestamp); + num_candidates = filter_candidates(trigger_id); // does nothing to timing } - 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); + struct combo_cfg *candidate_combo = candidates[0]; + LOG_DBG("combo: capturing trigger %d", trigger_id); + + int ret = capture_trigger(trigger); + update_timeout_task(); switch (num_candidates) { case 0: - cleanup(); + combo_cleanup(); return ret; - case 1: + case 1: // early accept when only one combo remains if (candidate_is_completely_pressed(candidate_combo)) { fully_pressed_combo = candidate_combo; - cleanup(); + combo_cleanup(); } return ret; default: @@ -459,44 +546,37 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_ } } -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; +static int trigger_state_up(int trigger_id, int64_t timestamp, struct trigger *trigger) { + combo_cleanup(); + if (!release_combo_key(trigger_id, timestamp)) { + release_fallback_behavior(trigger); } - return ZMK_EV_EVENT_BUBBLE; + return ZMK_BEHAVIOR_OPAQUE; } -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(); +int zmk_combo_trigger_behavior_invoked(int trigger_id, char *fallback_behavior_dev, + uint32_t fallback_param, + struct zmk_behavior_binding_event event, bool state) { + struct trigger trigger = {.trigger_id = trigger_id, + .fallback_behavior_dev = fallback_behavior_dev, + .fallback_param = fallback_param, + .event = event}; + if (state) { // keydown + return trigger_state_down(trigger_id, event.timestamp, &trigger); + } else { // keyup + return trigger_state_up(trigger_id, event.timestamp, &trigger); } - 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); +static int action_behavior_triggered_listener(const zmk_event_t *ev) { + struct zmk_action_behavior_triggered *data = as_zmk_action_behavior_triggered(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); + while (combo_cleanup()) { + continue; } + return ZMK_EV_EVENT_BUBBLE; } static int keycode_state_changed_listener(const zmk_event_t *eh) { @@ -508,29 +588,51 @@ static int keycode_state_changed_listener(const zmk_event_t *eh) { } int behavior_combo_listener(const zmk_event_t *eh) { - if (as_zmk_position_state_changed(eh) != NULL) { - return position_state_changed_listener(eh); + if (as_zmk_action_behavior_triggered(eh) != NULL) { + return action_behavior_triggered_listener(eh); } else if (as_zmk_keycode_state_changed(eh) != NULL) { return keycode_state_changed_listener(eh); + } else if (as_zmk_position_state_changed(eh) != NULL) { + struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); + if (ev->position == combo_position_changed_notification.position && + ev->timestamp == combo_position_changed_notification.timestamp) { + LOG_DBG("combo: catching position state %d", ev->position); + return ZMK_EV_EVENT_HANDLED; + } } return ZMK_EV_EVENT_BUBBLE; } ZMK_LISTENER(combo, behavior_combo_listener); ZMK_SUBSCRIPTION(combo, zmk_position_state_changed); +ZMK_SUBSCRIPTION(combo, zmk_action_behavior_triggered); ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed); +#define COMBO_GET_TRIGGER(idx, n) DT_PROP_BY_IDX(n, triggers, idx) + +#define COMBO_TRIGGER_ASSERT_VALID(node_id, prop, idx) \ + BUILD_ASSERT((DT_PROP_BY_IDX(node_id, prop, idx) < CONFIG_ZMK_COMBO_MAX_TRIGGER_NUM) && \ + (DT_PROP_BY_IDX(node_id, prop, idx) >= 0), \ + "Combo has an invalid trigger. "); + #define COMBO_INST(n) \ + BUILD_ASSERT(COND_CODE_1(DT_NODE_HAS_PROP(n, triggers), (DT_NODE_HAS_PROP(n, triggers)), \ + (DT_NODE_HAS_PROP(n, key_positions))), \ + "Combo is missing the 'triggers' property. "); \ + DT_FOREACH_PROP_ELEM(n, triggers, COMBO_TRIGGER_ASSERT_VALID) \ + BUILD_ASSERT(DT_PROP_LEN(n, triggers) <= CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO, \ + "Combo has too many triggers, adjust " \ + "CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO appropriately."); \ + static int32_t combo_config_##n##_triggers[DT_PROP_LEN(n, triggers)] = { \ + LISTIFY(DT_PROP_LEN(n, triggers), COMBO_GET_TRIGGER, (, ), 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), \ + .triggered_by = combo_config_##n##_triggers, \ + .triggered_by_len = DT_PROP_LEN(n, triggers), \ .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); diff --git a/app/src/events/action_behavior_triggered.c b/app/src/events/action_behavior_triggered.c new file mode 100644 index 00000000000..870e6a3fd4f --- /dev/null +++ b/app/src/events/action_behavior_triggered.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_action_behavior_triggered); \ No newline at end of file diff --git a/app/tests/combo/layer-filter-0/events.patterns b/app/tests/array/1-indicies/events.patterns similarity index 100% rename from app/tests/combo/layer-filter-0/events.patterns rename to app/tests/array/1-indicies/events.patterns diff --git a/app/tests/combo/layer-filter-1/keycode_events.snapshot b/app/tests/array/1-indicies/keycode_events.snapshot similarity index 100% rename from app/tests/combo/layer-filter-1/keycode_events.snapshot rename to app/tests/array/1-indicies/keycode_events.snapshot index bb47d85203c..b809163ddca 100644 --- a/app/tests/combo/layer-filter-1/keycode_events.snapshot +++ b/app/tests/array/1-indicies/keycode_events.snapshot @@ -1,4 +1,4 @@ pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 -pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/array/1-indicies/native_posix_64.keymap b/app/tests/array/1-indicies/native_posix_64.keymap new file mode 100644 index 00000000000..eed210634e0 --- /dev/null +++ b/app/tests/array/1-indicies/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/layer-filter-1/events.patterns b/app/tests/array/2a-nesting/events.patterns similarity index 100% rename from app/tests/combo/layer-filter-1/events.patterns rename to app/tests/array/2a-nesting/events.patterns diff --git a/app/tests/array/2a-nesting/keycode_events.snapshot b/app/tests/array/2a-nesting/keycode_events.snapshot new file mode 100644 index 00000000000..ad11d37958c --- /dev/null +++ b/app/tests/array/2a-nesting/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x29 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x29 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/array/2a-nesting/native_posix_64.keymap b/app/tests/array/2a-nesting/native_posix_64.keymap new file mode 100644 index 00000000000..a9e61e5626c --- /dev/null +++ b/app/tests/array/2a-nesting/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_PRESS(1,0,500) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/array/2b-nesting/events.patterns b/app/tests/array/2b-nesting/events.patterns new file mode 100644 index 00000000000..833100f6ac4 --- /dev/null +++ b/app/tests/array/2b-nesting/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p \ No newline at end of file diff --git a/app/tests/array/2b-nesting/keycode_events.snapshot b/app/tests/array/2b-nesting/keycode_events.snapshot new file mode 100644 index 00000000000..cc718d3de70 --- /dev/null +++ b/app/tests/array/2b-nesting/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x35 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x35 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/array/2b-nesting/native_posix_64.keymap b/app/tests/array/2b-nesting/native_posix_64.keymap new file mode 100644 index 00000000000..c68f894c6fd --- /dev/null +++ b/app/tests/array/2b-nesting/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/array/behavior_keymap.dtsi b/app/tests/array/behavior_keymap.dtsi new file mode 100644 index 00000000000..5747cc83642 --- /dev/null +++ b/app/tests/array/behavior_keymap.dtsi @@ -0,0 +1,32 @@ +#include +#include +#include + +/ { + behaviors { + arr: behavior_array { + compatible = "zmk,behavior-array"; + #binding-cells = <1>; + bindings = <&mt LEFT_SHIFT A &kp B &kp C &gresc>; + }; + + ht_bal: behavior_hold_tap_balanced { + compatible = "zmk,behavior-hold-tap"; + #binding-cells = <2>; + flavor = "balanced"; + tapping-term-ms = <200>; + bindings = <&arr>, <&arr>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &arr 0 &arr 1 + &ht_bal 2 3 &arr 3 + >; + }; + }; +}; diff --git a/app/tests/combo/combos-and-holdtaps-0/native_posix_64.keymap b/app/tests/combo/combos-and-holdtaps-0/native_posix_64.keymap index 6f9f9860f27..69a4b2bda0b 100644 --- a/app/tests/combo/combos-and-holdtaps-0/native_posix_64.keymap +++ b/app/tests/combo/combos-and-holdtaps-0/native_posix_64.keymap @@ -17,7 +17,7 @@ first so the decision to hold or tap can be made. combo_two { timeout-ms = <100>; - key-positions = <1 2>; + triggers = <1 2>; bindings = <&kp Y>; }; }; @@ -27,8 +27,8 @@ first so the decision to hold or tap can be made. default_layer { bindings = < - &mt LEFT_CONTROL A &kp B - &kp C &none + &mt LEFT_CONTROL A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/combos-and-holdtaps-1/native_posix_64.keymap b/app/tests/combo/combos-and-holdtaps-1/native_posix_64.keymap index 0982d34befa..3b5a5ea7efa 100644 --- a/app/tests/combo/combos-and-holdtaps-1/native_posix_64.keymap +++ b/app/tests/combo/combos-and-holdtaps-1/native_posix_64.keymap @@ -12,17 +12,31 @@ compatible = "zmk,combos"; combo_two { timeout-ms = <100>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp Y>; }; }; + behaviors { + arr: behavior_array { + compatible = "zmk,behavior-array"; + #binding-cells = <1>; + bindings = <&mt LEFT_CONTROL A>; + }; + combo: combo_trigger_with_array { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or Mod Tap"; + fallback-behavior = <&arr>; + }; + }; + keymap { compatible = "zmk,keymap"; default_layer { bindings = < - &mt LEFT_CONTROL A &kp B + &combo 0 0 &combo_kp 1 B &kp C &none >; }; diff --git a/app/tests/combo/combos-and-holdtaps-2/native_posix_64.keymap b/app/tests/combo/combos-and-holdtaps-2/native_posix_64.keymap index 6feebf2f76f..b2281f67333 100644 --- a/app/tests/combo/combos-and-holdtaps-2/native_posix_64.keymap +++ b/app/tests/combo/combos-and-holdtaps-2/native_posix_64.keymap @@ -14,23 +14,35 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <100>; - key-positions = <0 2>; + triggers = <0 2>; bindings = <&kp Y>; }; combo_two { timeout-ms = <100>; - key-positions = <1 3>; + triggers = <1 3>; bindings = <&kp Z>; }; }; + behaviors { + arr: behavior_array { + compatible = "zmk,behavior-array"; + #binding-cells = <1>; + bindings = <&mt LEFT_CONTROL A &mt RIGHT_CONTROL B &none>; + }; + combo: combo_trigger1{ + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + fallback-behavior = <&arr>; + }; + }; + keymap { compatible = "zmk,keymap"; default_layer { bindings = < - &mt LEFT_CONTROL A &mt RIGHT_CONTROL B - &none &none + &combo 0 0 &combo 1 1 &combo 2 2 &combo 3 2 >; }; }; diff --git a/app/tests/combo/combos-and-holdtaps-3/native_posix_64.keymap b/app/tests/combo/combos-and-holdtaps-3/native_posix_64.keymap index fbbd7a9e501..a2d4415225c 100644 --- a/app/tests/combo/combos-and-holdtaps-3/native_posix_64.keymap +++ b/app/tests/combo/combos-and-holdtaps-3/native_posix_64.keymap @@ -11,7 +11,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <40>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp X>; }; }; @@ -21,7 +21,7 @@ default_layer { bindings = < - &kp A &kp B + &combo_kp 0 A &combo_kp 1 B &mt RSHFT RET &kp C >; }; diff --git a/app/tests/combo/combos-and-holdtaps-4/native_posix_64.keymap b/app/tests/combo/combos-and-holdtaps-4/native_posix_64.keymap index b3e987cf277..e5012c4e0f2 100644 --- a/app/tests/combo/combos-and-holdtaps-4/native_posix_64.keymap +++ b/app/tests/combo/combos-and-holdtaps-4/native_posix_64.keymap @@ -8,7 +8,7 @@ combos { \ compatible = "zmk,combos"; \ combo_ ## name { \ - key-positions = ; \ + triggers = ; \ bindings = ; \ timeout-ms = ; \ }; \ @@ -20,14 +20,34 @@ ZMK_COMBO(dllr, &kp DLLR, 1 3, 50) ZMK_COMBO(tilde, &kp TILDE, 3 4, 50) / { + + macros { + ZMK_MACRO(combo_macro, bindings + = <¯o_press &mt LSHFT T> + , <¯o_pause_for_release> + , <¯o_release &mt LSHFT T>; + ) + }; + + behaviors { + combo1: combo_trigger1{ + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + fallback-behavior = <&none>; + }; + combo_n: combo_trigger2{ + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + fallback-behavior = <&combo_macro>; + }; + }; + keymap { compatible = "zmk,keymap"; default_layer { bindings = < - &none &none - &kp A &mt LSHFT T - &none + &combo1 0 0 &combo1 1 0 &combo_kp 2 A &combo_n 3 0 &combo1 4 0 >; }; }; diff --git a/app/tests/combo/layer-filter-0/keycode_events.snapshot b/app/tests/combo/layer-filter-0/keycode_events.snapshot deleted file mode 100644 index 21bf0c3ffdb..00000000000 --- a/app/tests/combo/layer-filter-0/keycode_events.snapshot +++ /dev/null @@ -1,8 +0,0 @@ -pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 -released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 -pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 -released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 -pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 -released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 -pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 -released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/layer-filter-0/native_posix_64.keymap b/app/tests/combo/layer-filter-0/native_posix_64.keymap deleted file mode 100644 index 12946183249..00000000000 --- a/app/tests/combo/layer-filter-0/native_posix_64.keymap +++ /dev/null @@ -1,77 +0,0 @@ -#include -#include -#include - -/* it is useful to set timeout to a large value when attaching a debugger. */ -#define TIMEOUT (60*60*1000) - -/ { - combos { - compatible = "zmk,combos"; - combo_one { - timeout-ms = ; - key-positions = <0 1>; - bindings = <&kp X>; - layers = <0>; - }; - - combo_two { - timeout-ms = ; - key-positions = <0 1>; - bindings = <&kp Y>; - layers = <1>; - }; - - combo_three { - timeout-ms = ; - key-positions = <0 2>; - bindings = <&kp Z>; - }; - }; - - keymap { - compatible = "zmk,keymap"; - - default_layer { - bindings = < - &kp A &kp B - &kp C &tog 1 - >; - }; - - filtered_layer { - bindings = < - &kp A &kp B - &kp C &tog 0 - >; - }; - }; -}; - -&kscan { - events = < - /* Combo One */ - ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_PRESS(0,1,10) - ZMK_MOCK_RELEASE(0,0,10) - ZMK_MOCK_RELEASE(0,1,10) - /* Combo Three */ - ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_PRESS(1,1,10) - ZMK_MOCK_RELEASE(0,0,10) - ZMK_MOCK_RELEASE(1,1,10) - /* Toggle Layer */ - ZMK_MOCK_PRESS(1,1,10) - ZMK_MOCK_RELEASE(1,1,10) - /* Combo Two */ - ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_PRESS(0,1,10) - ZMK_MOCK_RELEASE(0,0,10) - ZMK_MOCK_RELEASE(0,1,10) - /* Combo Three */ - ZMK_MOCK_PRESS(0,0,10) - ZMK_MOCK_PRESS(1,1,10) - ZMK_MOCK_RELEASE(0,0,10) - ZMK_MOCK_RELEASE(1,1,10) - >; -}; \ No newline at end of file diff --git a/app/tests/combo/multiple-timeouts/native_posix_64.keymap b/app/tests/combo/multiple-timeouts/native_posix_64.keymap index 0e3ae996a19..d77d9ba3b1d 100644 --- a/app/tests/combo/multiple-timeouts/native_posix_64.keymap +++ b/app/tests/combo/multiple-timeouts/native_posix_64.keymap @@ -7,12 +7,12 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; combo_two { timeout-ms = <120>; - key-positions = <0 1 2>; + triggers = <0 1 2>; bindings = <&kp C>; }; }; @@ -22,8 +22,8 @@ default_layer { bindings = < - &kp A &kp B - &none &none + &combo_kp 0 A &combo_kp 1 B + &none &none >; }; }; diff --git a/app/tests/combo/overlapping-combos-0/native_posix_64.keymap b/app/tests/combo/overlapping-combos-0/native_posix_64.keymap index a9a229fb3a2..6ed3f9a2895 100644 --- a/app/tests/combo/overlapping-combos-0/native_posix_64.keymap +++ b/app/tests/combo/overlapping-combos-0/native_posix_64.keymap @@ -18,19 +18,19 @@ compatible = "zmk,combos"; combo_one { timeout-ms = ; - key-positions = <0 1 2>; + triggers = <0 1 2>; bindings = <&kp X>; }; combo_two { timeout-ms = ; - key-positions = <0 2>; + triggers = <0 2>; bindings = <&kp Y>; }; combo_three { timeout-ms = ; - key-positions = <1>; + triggers = <1>; bindings = <&kp Z>; }; }; @@ -40,8 +40,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/overlapping-combos-1/native_posix_64.keymap b/app/tests/combo/overlapping-combos-1/native_posix_64.keymap index f3b0ab975b1..9e39368164f 100644 --- a/app/tests/combo/overlapping-combos-1/native_posix_64.keymap +++ b/app/tests/combo/overlapping-combos-1/native_posix_64.keymap @@ -13,13 +13,13 @@ compatible = "zmk,combos"; combo_two { timeout-ms = <50>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp Y>; }; combo_three { timeout-ms = <100>; - key-positions = <0 1 2>; + triggers = <0 1 2>; bindings = <&kp X>; }; }; @@ -29,8 +29,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/overlapping-combos-2/native_posix_64.keymap b/app/tests/combo/overlapping-combos-2/native_posix_64.keymap index beed222e890..95f489afc91 100644 --- a/app/tests/combo/overlapping-combos-2/native_posix_64.keymap +++ b/app/tests/combo/overlapping-combos-2/native_posix_64.keymap @@ -13,13 +13,13 @@ compatible = "zmk,combos"; combo_two { timeout-ms = <100>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp Y>; }; combo_four { timeout-ms = <100>; - key-positions = <0 1 2 3>; + triggers = <0 1 2 3>; bindings = <&kp W>; }; @@ -30,8 +30,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/overlapping-combos-3/native_posix_64.keymap b/app/tests/combo/overlapping-combos-3/native_posix_64.keymap index 2e7e4225bb2..d1f49ad8168 100644 --- a/app/tests/combo/overlapping-combos-3/native_posix_64.keymap +++ b/app/tests/combo/overlapping-combos-3/native_posix_64.keymap @@ -13,14 +13,14 @@ compatible = "zmk,combos"; combo_two { timeout-ms = <100>; - key-positions = <1 2>; + triggers = <1 2>; bindings = <&kp Y>; }; combo_four { timeout-ms = <100>; - key-positions = <0 1 2 3>; + triggers = <0 1 2 3>; bindings = <&kp W>; }; @@ -31,8 +31,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap b/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap index 06a67b88134..0aa995c0cba 100644 --- a/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap +++ b/app/tests/combo/overlapping-combos-4-different-timeouts/native_posix_64.keymap @@ -14,17 +14,17 @@ // Intentionally out of order in the config, to make sure 'combo.c' handles it properly combo_40 { timeout-ms = <40>; - key-positions = ; + triggers = ; bindings = <&kp Z>; }; combo_20 { timeout-ms = <20>; - key-positions = ; + triggers = ; bindings = <&kp X>; }; combo_30 { timeout-ms = <30>; - key-positions = ; + triggers = ; bindings = <&kp Y>; }; @@ -35,8 +35,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &kp D + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &combo_kp 3 D >; }; }; diff --git a/app/tests/combo/partially-overlapping-combos/native_posix_64.keymap b/app/tests/combo/partially-overlapping-combos/native_posix_64.keymap index d3151382ece..9a7af665694 100644 --- a/app/tests/combo/partially-overlapping-combos/native_posix_64.keymap +++ b/app/tests/combo/partially-overlapping-combos/native_posix_64.keymap @@ -7,19 +7,19 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp X>; }; combo_two { timeout-ms = <30>; - key-positions = <0 2>; + triggers = <0 2>; bindings = <&kp Y>; }; combo_three { timeout-ms = <30>; - key-positions = <3>; + triggers = <3>; bindings = <&kp Z>; }; }; @@ -29,8 +29,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &none >; }; }; diff --git a/app/tests/combo/press-release-interrupting-middle-key/events.patterns b/app/tests/combo/press-release-interrupting-middle-key/events.patterns new file mode 100644 index 00000000000..b1342af4d97 --- /dev/null +++ b/app/tests/combo/press-release-interrupting-middle-key/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/press-release-interrupting-middle-key/keycode_events.snapshot b/app/tests/combo/press-release-interrupting-middle-key/keycode_events.snapshot new file mode 100644 index 00000000000..69befff3c19 --- /dev/null +++ b/app/tests/combo/press-release-interrupting-middle-key/keycode_events.snapshot @@ -0,0 +1,6 @@ +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/layer-filter-1/native_posix_64.keymap b/app/tests/combo/press-release-interrupting-middle-key/native_posix_64.keymap similarity index 55% rename from app/tests/combo/layer-filter-1/native_posix_64.keymap rename to app/tests/combo/press-release-interrupting-middle-key/native_posix_64.keymap index 6d4a3021278..b0c80cff961 100644 --- a/app/tests/combo/layer-filter-1/native_posix_64.keymap +++ b/app/tests/combo/press-release-interrupting-middle-key/native_posix_64.keymap @@ -2,17 +2,13 @@ #include #include -/* it is useful to set timeout to a large value when attaching a debugger. */ -#define TIMEOUT (60*60*1000) - / { combos { compatible = "zmk,combos"; combo_one { - timeout-ms = ; - key-positions = <0 1>; - bindings = <&kp X>; - layers = <1>; + timeout-ms = <80>; + triggers = <0 2>; + bindings = <&kp Z>; }; }; @@ -22,7 +18,7 @@ default_layer { bindings = < &kp A &kp B - &kp C &tog 1 + &kp C &kp D >; }; }; @@ -30,10 +26,11 @@ &kscan { events = < - /* Combo One */ ZMK_MOCK_PRESS(0,0,10) ZMK_MOCK_PRESS(0,1,10) - ZMK_MOCK_RELEASE(0,0,10) - ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_RELEASE(0,1,100) + ZMK_MOCK_RELEASE(0,0,100) >; -}; \ No newline at end of file +}; diff --git a/app/tests/combo/press-release-long-combo-complete/native_posix_64.keymap b/app/tests/combo/press-release-long-combo-complete/native_posix_64.keymap index 85cb6475cae..d8e89f43c06 100644 --- a/app/tests/combo/press-release-long-combo-complete/native_posix_64.keymap +++ b/app/tests/combo/press-release-long-combo-complete/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <80>; - key-positions = <0 1 2 3>; + triggers = <0 1 2 3>; bindings = <&kp Z>; }; }; @@ -17,8 +17,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &kp D + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &combo_kp 3 D >; }; }; diff --git a/app/tests/combo/press-release-long-combo-incomplete/native_posix_64.keymap b/app/tests/combo/press-release-long-combo-incomplete/native_posix_64.keymap index 49b92968602..fdd1318c1de 100644 --- a/app/tests/combo/press-release-long-combo-incomplete/native_posix_64.keymap +++ b/app/tests/combo/press-release-long-combo-incomplete/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <80>; - key-positions = <0 1 2 3>; + triggers = <0 1 2 3>; bindings = <&kp Z>; }; }; @@ -17,8 +17,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &kp D + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &combo_kp 3 D >; }; }; diff --git a/app/tests/combo/press-release-long-combo-wrong-last-key/native_posix_64.keymap b/app/tests/combo/press-release-long-combo-wrong-last-key/native_posix_64.keymap index 61787322cf5..fa2f3dc614a 100644 --- a/app/tests/combo/press-release-long-combo-wrong-last-key/native_posix_64.keymap +++ b/app/tests/combo/press-release-long-combo-wrong-last-key/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <80>; - key-positions = <0 1 2>; + triggers = <0 1 2>; bindings = <&kp Z>; }; }; @@ -17,8 +17,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &kp D + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &kp D >; }; }; diff --git a/app/tests/combo/press-release/native_posix_64.keymap b/app/tests/combo/press-release/native_posix_64.keymap index 783dcf00437..ac13f6fcd6d 100644 --- a/app/tests/combo/press-release/native_posix_64.keymap +++ b/app/tests/combo/press-release/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; }; @@ -17,7 +17,7 @@ default_layer { bindings = < - &kp A &kp B + &combo_kp 0 A &combo_kp 1 B &none &none >; }; diff --git a/app/tests/combo/press-timeout/native_posix_64.keymap b/app/tests/combo/press-timeout/native_posix_64.keymap index c9cd2331e34..4ee1355ce8d 100644 --- a/app/tests/combo/press-timeout/native_posix_64.keymap +++ b/app/tests/combo/press-timeout/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; }; @@ -17,7 +17,7 @@ default_layer { bindings = < - &kp A &kp B + &combo_kp 0 A &combo_kp 1 B &none &none >; }; diff --git a/app/tests/combo/press1-press2-release1-release2/native_posix_64.keymap b/app/tests/combo/press1-press2-release1-release2/native_posix_64.keymap index 55d93823dfe..3e5beaa4076 100644 --- a/app/tests/combo/press1-press2-release1-release2/native_posix_64.keymap +++ b/app/tests/combo/press1-press2-release1-release2/native_posix_64.keymap @@ -7,13 +7,13 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; combo_two { timeout-ms = <30>; - key-positions = <2 3>; + triggers = <2 3>; bindings = <&kp D>; }; }; @@ -23,8 +23,8 @@ default_layer { bindings = < - &kp A &kp B - &kp Z &kp Y + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 Z &combo_kp 3 Y >; }; }; diff --git a/app/tests/combo/press1-press2-release2-release1/native_posix_64.keymap b/app/tests/combo/press1-press2-release2-release1/native_posix_64.keymap index ace63a16a80..b6fb401c4c2 100644 --- a/app/tests/combo/press1-press2-release2-release1/native_posix_64.keymap +++ b/app/tests/combo/press1-press2-release2-release1/native_posix_64.keymap @@ -7,13 +7,13 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; combo_two { timeout-ms = <30>; - key-positions = <2 3>; + triggers = <2 3>; bindings = <&kp D>; }; }; @@ -23,8 +23,8 @@ default_layer { bindings = < - &kp A &kp B - &kp Z &kp Y + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 Z &combo_kp 3 Y >; }; }; diff --git a/app/tests/combo/press1-release1-press2-release2/native_posix_64.keymap b/app/tests/combo/press1-release1-press2-release2/native_posix_64.keymap index 8b59792b047..b63b5721b91 100644 --- a/app/tests/combo/press1-release1-press2-release2/native_posix_64.keymap +++ b/app/tests/combo/press1-release1-press2-release2/native_posix_64.keymap @@ -7,13 +7,13 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; }; combo_two { timeout-ms = <30>; - key-positions = <2 3>; + triggers = <2 3>; bindings = <&kp D>; }; }; @@ -23,8 +23,8 @@ default_layer { bindings = < - &kp A &kp B - &kp Z &kp Y + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 Z &combo_kp 3 Y >; }; }; diff --git a/app/tests/combo/require-prior-idle/native_posix_64.keymap b/app/tests/combo/require-prior-idle/native_posix_64.keymap index 72801f74950..5c35cd673b9 100644 --- a/app/tests/combo/require-prior-idle/native_posix_64.keymap +++ b/app/tests/combo/require-prior-idle/native_posix_64.keymap @@ -7,14 +7,14 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <50>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp X>; require-prior-idle-ms = <100>; }; combo_two { timeout-ms = <50>; - key-positions = <0 2>; + triggers = <0 2>; bindings = <&kp Y>; }; }; @@ -24,8 +24,8 @@ default_layer { bindings = < - &kp A &kp B - &kp C &kp D + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &combo_kp 3 D >; }; }; diff --git a/app/tests/combo/simultaneous-combos/events.patterns b/app/tests/combo/simultaneous-combos/events.patterns new file mode 100644 index 00000000000..b1342af4d97 --- /dev/null +++ b/app/tests/combo/simultaneous-combos/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/simultaneous-combos/keycode_events.snapshot b/app/tests/combo/simultaneous-combos/keycode_events.snapshot new file mode 100644 index 00000000000..a885ea1765e --- /dev/null +++ b/app/tests/combo/simultaneous-combos/keycode_events.snapshot @@ -0,0 +1,18 @@ +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1B implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/combo/simultaneous-combos/native_posix_64.keymap b/app/tests/combo/simultaneous-combos/native_posix_64.keymap new file mode 100644 index 00000000000..5ac36448ea7 --- /dev/null +++ b/app/tests/combo/simultaneous-combos/native_posix_64.keymap @@ -0,0 +1,73 @@ +#include +#include +#include + +/* + * This tests how multiple combos being pressed simultaneously interact + * Current expected behavior is that only one combo can be triggered at once + * Hence, interwoven key presses should output all 4 individual key presses + * Pressing combo 2 between combo 1's keys should result in a combo 1 key, then combo2, then the other combo1 key + * Pressing one combo directly after the other should result in both being output one after the other + */ + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + triggers = <0 1>; + bindings = <&kp X>; + }; + + combo_two { + timeout-ms = <30>; + triggers = <2 3>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 C &combo_kp 3 D + >; + }; + }; +}; + +&kscan { + events = < + /* interwoven combo key presses */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + + /* combo 2 between combo 1 presses */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + + /* combo 2 after combo 1 */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; diff --git a/app/tests/combo/slowrelease-disabled/native_posix_64.keymap b/app/tests/combo/slowrelease-disabled/native_posix_64.keymap index cfea0cd60e4..70f26f2b1be 100644 --- a/app/tests/combo/slowrelease-disabled/native_posix_64.keymap +++ b/app/tests/combo/slowrelease-disabled/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; /* no slow-release! */ }; @@ -18,8 +18,8 @@ default_layer { bindings = < - &kp A &kp B - &kp D &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 D &none >; }; }; diff --git a/app/tests/combo/slowrelease-enabled/native_posix_64.keymap b/app/tests/combo/slowrelease-enabled/native_posix_64.keymap index e57bae60c88..17438d372e7 100644 --- a/app/tests/combo/slowrelease-enabled/native_posix_64.keymap +++ b/app/tests/combo/slowrelease-enabled/native_posix_64.keymap @@ -7,7 +7,7 @@ compatible = "zmk,combos"; combo_one { timeout-ms = <30>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp C>; slow-release; }; @@ -18,8 +18,8 @@ default_layer { bindings = < - &kp A &kp B - &kp D &none + &combo_kp 0 A &combo_kp 1 B + &combo_kp 2 D &none >; }; }; diff --git a/app/tests/tap-dance/3b-tap-int-seq/keycode_events.snapshot b/app/tests/tap-dance/3b-tap-int-seq/keycode_events.snapshot index 31113ffc4aa..6c3ed1e1bf2 100644 --- a/app/tests/tap-dance/3b-tap-int-seq/keycode_events.snapshot +++ b/app/tests/tap-dance/3b-tap-int-seq/keycode_events.snapshot @@ -1,10 +1,10 @@ td_binding_pressed: 2 created new tap dance td_binding_pressed: 2 tap dance pressed kp_pressed: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 2 tap dance keybind released td_binding_pressed: 1 created new tap dance td_binding_pressed: 1 tap dance pressed kp_pressed: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00 -td_binding_released: 2 tap dance keybind released kp_released: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00 td_binding_released: 1 tap dance keybind released kp_released: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/behavior_keymap.dtsi b/app/tests/tap-dance/behavior_keymap.dtsi index e45aba4001a..e37ea41d594 100644 --- a/app/tests/tap-dance/behavior_keymap.dtsi +++ b/app/tests/tap-dance/behavior_keymap.dtsi @@ -40,6 +40,20 @@ tapping-term-ms = <200>; bindings = <&kp S>; }; + + combo_m: combo_trigger_tap_dance_mixed { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or tap dance mixed"; + fallback-behavior = <&tdm>; + }; + + combo_s: combo_trigger_tap_dance_single { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or tap dance single"; + fallback-behavior = <&tds>; + }; }; combos { @@ -47,7 +61,7 @@ td_combo { bindings = <&tdb>; - key-positions = <0 1>; + triggers = <0 1>; timeout-ms = <50>; }; }; @@ -57,8 +71,8 @@ default_layer { bindings = < - &tdm &tds - &tdb &td2>; + &combo_m 0 0 &combo_s 1 0 + &tdb &td2>; }; }; }; diff --git a/docs/docs/config/combos.md b/docs/docs/config/combos.md index d773628db2a..db34ba6112c 100644 --- a/docs/docs/config/combos.md +++ b/docs/docs/config/combos.md @@ -11,15 +11,16 @@ See [Configuration Overview](index.md) for instructions on how to change these s Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) -| Config | Type | Description | Default | -| ------------------------------------- | ---- | -------------------------------------------------------------- | ------- | -| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 | -| `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` | int | Maximum number of active combos that use the same key position | 5 | -| `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` | int | Maximum number of keys to press to activate a combo | 4 | +| Config | Type | Description | Default | +| ----------------------------------------- | ---- | --------------------------------------------------------------------- | ------- | +| `ZMK_COMBO_MAX_TRIGGER_NUM` | int | Upper bound of numbers that can be passed to a combo trigger behavior | 20 | +| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 | +| `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER` | int | Maximum number of active combos that use the same trigger | 5 | +| `CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO` | int | Maximum number of triggers required to activate a combo | 4 | -If `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` is 5, you can have 5 separate combos that use position `0`, 5 combos that use position `1`, and so on. +If `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_TRIGGER` is 5, you can have 5 separate combos that use trigger `0`, 5 combos that use trigger `1`, and so on. -If you want a combo that triggers when pressing 5 keys, you must set `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` to 5. +If you want a combo that triggers after activating 5 different [combo triggers](../keymaps/behaviors/combo-trigger.md), you must set `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` to 5. ## Devicetree @@ -31,13 +32,12 @@ The `zmk,combos` node itself has no properties. It should have one child node pe Each child node can have the following properties: -| Property | Type | Description | Default | -| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `bindings` | phandle-array | A [behavior](../keymaps/index.mdx#behaviors) to run when the combo is triggered | | -| `key-positions` | array | A list of key position indices for the keys which should trigger the combo | | -| `timeout-ms` | int | All the keys in `key-positions` must be pressed within this time in milliseconds to trigger the combo | 50 | -| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) | -| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` | +| Property | Type | Description | Default | +| ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `bindings` | phandle-array | A [behavior](../keymaps/index.mdx#behaviors) to run when the combo is triggered | | +| `triggers` | array | A list of trigger ids which should trigger the combo | | +| `timeout-ms` | int | All the triggers in `triggers` must be activated within this time in milliseconds to trigger the combo | 50 | +| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a trigger in the combo, the trigger will not be considered for the combo | -1 (disabled) | +| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false | -The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5. +The `triggers` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_TRIGGERS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when activating 5 triggers, then you must change the setting to 5. diff --git a/docs/docs/keymaps/behaviors/array.md b/docs/docs/keymaps/behaviors/array.md new file mode 100644 index 00000000000..a5588d9a8e9 --- /dev/null +++ b/docs/docs/keymaps/behaviors/array.md @@ -0,0 +1,66 @@ +--- +title: Array of Behaviors +sidebar_label: Array +--- + +## Summary + +An array of behaviors is meant not to be placed in your keymap directly, but rather to simplify the usage of other behaviors such as [hold-tap](hold-tap.mdx), [combo-trigger](combo-trigger.md), or [mod-morph](mod-morph.md). + +Invoking an array of behaviors with a particular integer will trigger the behavior at that location in the array (indexed from 0). + +## Mod-Morph + +### Configuration + +Below is an example of how to implement an array with three elements. + +```dts +/ { + arr: behavior_array { + compatible = "zmk,behavior-array"; + #binding-cells = <1>; + bindings = <&mt LEFT_CONTROL A &kp B < 1 C>; + }; +}; +``` + +The above behavior array could be triggered like so: + +```dts +&arr 0 +``` + +The above would act like `&mt LEFT_CONTROL A`. + +```dts +&arr 1 +``` + +The above would act like `&kp B`. + +```dts +&arr 2 +``` + +The above would act like `< 1 C`. + +The `&arr X` call could happen in your keymap, but it is more useful if it is used e.g. as the `fallback-behavior` parameter of a [combo-trigger](combo-trigger.md): + +```dts +/ { + behaviors { + combo_mt: combo_trigger_or_key_press { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or Mod Tap"; + fallback-behavior = <&mt_arr>; + }; + mt_arr: mod_tap_behavior_array { + compatible = "zmk,behavior-array"; + #binding-cells = <1>; + bindings = <&mt LEFT_CONTROL A &mt LEFT_CONTROL B &mt LEFT_CONTROL C>; + }; + }; +}; +``` diff --git a/docs/docs/keymaps/behaviors/combo-trigger.md b/docs/docs/keymaps/behaviors/combo-trigger.md new file mode 100644 index 00000000000..b6e1dd0e4dc --- /dev/null +++ b/docs/docs/keymaps/behaviors/combo-trigger.md @@ -0,0 +1,52 @@ +--- +title: Combo Trigger Behavior +sidebar_label: Combo Trigger +--- + +## Summary + +The combo trigger behavior is a special type of behavior that is used together with the [`compatible = "zmk,combos";`](../combos.md) node to add combos to your keymap. + +## Combo Trigger + +### Configuration + +Below is an example of how to implement the combo trigger "Combo or Key Press". When assigned to a key, it will send the combo trigger id of the first parameter passed to it on to the `combos` node. If no combo is triggered, the fallback behavior is triggered using the second parameter passed to the combo trigger. + +```dts +/ { + behaviors { + combo_kp: combo_trigger_or_key_press { + compatible = "zmk,behavior-combo-trigger"; + #binding-cells = <2>; + display-name = "Combo or Key Press"; + fallback-behavior = <&kp>; + }; + }; +}; +``` + +Note that this specific combo trigger behavior exists in ZMK by default using the binding `&combo_kp`. + +### Behavior Binding + +- Reference: `&combo_kp` +- Parameter: None + +Example: + +```dts +&combo_kp 1 A +``` + +When activated, the `combos` node receives the trigger `1`. If no combo is triggered, the activation acts like a `&kp A` instead. + +### Fallback Behavior + +It is assumed that the behavior in `fallback-behavior` accepts a single parameter as an argument. Hence the behavior should always be given without any arguments. If the behavior accepts no arguments, two parameters should still be passed to the combo trigger: + +```dts +&combo_no_param_fallback 1 0 +``` + +If the behavior you wish to have as a fallback-behavior accepts two parameters as arguments, it is recommended that you make use of the [array behavior](array.md). diff --git a/docs/docs/keymaps/combos.md b/docs/docs/keymaps/combos.md index 9616b5eb082..9ad96da75d1 100644 --- a/docs/docs/keymaps/combos.md +++ b/docs/docs/keymaps/combos.md @@ -8,7 +8,7 @@ Combo keys are a way to combine multiple keypresses to output a different key. F ### Configuration -Combos configured in your `.keymap` file, but are separate from the `keymap` node found there, since they are processed before the normal keymap. They are specified like this: +Combos configured in your `.keymap` file, but are separate from the `keymap` node found there. Alongside other options, they define a list of `triggers` that need to be activated by [combo-triggers](behaviors/combo-trigger.md) within `timeout-ms` seconds of each other to activate the output found in `bindings`. They are specified like this: ```dts / { @@ -16,7 +16,7 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod compatible = "zmk,combos"; combo_esc { timeout-ms = <50>; - key-positions = <0 1>; + triggers = <0 1>; bindings = <&kp ESC>; }; }; @@ -25,16 +25,15 @@ Combos configured in your `.keymap` file, but are separate from the `keymap` nod - The name of the combo doesn't really matter, but convention is to start the node name with `combo_`. - The `compatible` property should always be `"zmk,combos"` for combos. -- All the keys in `key-positions` must be pressed within `timeout-ms` milliseconds to trigger the combo. +- All the [combo-triggers](behaviors/combo-trigger.md) in `triggers` must be pressed within `timeout-ms` milliseconds to trigger the combo. - `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. -- `layers = <0 1...>` will allow limiting a combo to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. - `bindings` is the behavior that is activated when the behavior is pressed. - (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. - (advanced) you can specify a `require-prior-idle-ms` value much like for [hold-taps](behaviors/hold-tap.mdx#require-prior-idle-ms). If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the combo will not trigger. :::info -Key positions are numbered like the keys in your keymap, starting at 0. So, if the first key in your keymap is `Q`, this key is in position `0`. The next key (possibly `W`) will have position 1, etcetera. +By default, triggers can be any non-negative number below 20. To increase (or decrease for memory savings) this limit, you will need to adjust the corresponding [`Kconfig flag`](../config/combos.md). :::