Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(combos): Overhaul the combo system #2765

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -66,14 +65,17 @@ 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)
target_sources(app PRIVATE src/endpoints.c)
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)
Expand All @@ -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)

Expand Down
15 changes: 10 additions & 5 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions app/Kconfig.behaviors
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions app/dts/behaviors.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
#include <behaviors/soft_off.dtsi>
#include <behaviors/studio_unlock.dtsi>
#include <behaviors/mouse_keys.dtsi>
#include <behaviors/combo_kp.dtsi>

21 changes: 21 additions & 0 deletions app/dts/behaviors/combo_kp.dtsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <dt-bindings/zmk/behaviors.h>

/ {
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>;
};
};
};
13 changes: 13 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-array.yaml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-combo-trigger.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 1 addition & 4 deletions app/dts/bindings/zmk,combos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ child-binding:
bindings:
type: phandle-array
required: true
key-positions:
triggers:
type: array
required: true
timeout-ms:
Expand All @@ -23,6 +23,3 @@ child-binding:
default: -1
slow-release:
type: boolean
layers:
type: array
default: [-1]
24 changes: 24 additions & 0 deletions app/include/drivers/behavior.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions app/include/zmk/combos.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
26 changes: 26 additions & 0 deletions app/include/zmk/events/action_behavior_triggered.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <zephyr/kernel.h>
#include <zmk/event_manager.h>
#include <zmk/behavior.h>

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});
}
42 changes: 40 additions & 2 deletions app/src/behavior.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <zmk/matrix.h>

#include <zmk/events/position_state_changed.h>
#include <zmk/events/action_behavior_triggered.h>

#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
79 changes: 79 additions & 0 deletions app/src/behaviors/behavior_array.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_behavior_array

#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>
#include <zmk/keymap.h>

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
1 change: 1 addition & 0 deletions app/src/behaviors/behavior_backlight.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/behaviors/behavior_bt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/src/behaviors/behavior_caps_word.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading