diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60c502fc..2361edb7 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -48,6 +48,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/hid.c) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_TURBO_KEY app PRIVATE src/behaviors/behavior_turbo_key.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_HOLD_TAP app PRIVATE src/behaviors/behavior_hold_tap.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_STICKY_KEY app PRIVATE src/behaviors/behavior_sticky_key.c) target_sources(app PRIVATE src/behaviors/behavior_caps_word.c) diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index da9bcc41..724c5b17 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -134,3 +134,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_TURBO_KEY + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_TURBO_KEY_ENABLED || DT_HAS_ZMK_BEHAVIOR_TURBO_KEY_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_TURBO_KEY_TWO_PARAM_ENABLED diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d..cd8cfd72 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/turbo.dtsi b/app/dts/behaviors/turbo.dtsi new file mode 100644 index 00000000..6e74110c --- /dev/null +++ b/app/dts/behaviors/turbo.dtsi @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define TURBO_PLACEHOLDER 0 + +#define ZMK_TURBO(name,...) \ +name: name { \ + compatible = "zmk,behavior-turbo-key"; \ + #binding-cells = <0>; \ + __VA_ARGS__ \ +}; + +#define ZMK_TURBO1(name,...) \ +name: name { \ + compatible = "zmk,behavior-turbo-key-one-param"; \ + #binding-cells = <1>; \ + __VA_ARGS__ \ +}; + +#define ZMK_TURBO2(name,...) \ +name: name { \ + compatible = "zmk,behavior-turbo-key-two-param"; \ + #binding-cells = <2>; \ + __VA_ARGS__ \ +}; + +/ { + behaviors { + turbo_param_1to1: turbo_param_1to1 { + compatible = "zmk,turbo-param-1to1"; + #binding-cells = <0>; + }; + + turbo_param_1to2: turbo_param_1to2 { + compatible = "zmk,turbo-param-1to2"; + #binding-cells = <0>; + }; + + turbo_param_2to1: turbo_param_2to1 { + compatible = "zmk,turbo-param-2to1"; + #binding-cells = <0>; + }; + + turbo_param_2to2: turbo_param_2to2 { + compatible = "zmk,turbo-param-2to2"; + #binding-cells = <0>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/turbo_base.yaml b/app/dts/bindings/behaviors/turbo_base.yaml new file mode 100644 index 00000000..6470ce7e --- /dev/null +++ b/app/dts/bindings/behaviors/turbo_base.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +properties: + bindings: + type: phandle-array + required: true + wait-ms: + type: int + default: 200 + tap-ms: + type: int + default: 5 + toggle-term-ms: + type: int + default: -1 diff --git a/app/dts/bindings/behaviors/zmk,behavior-turbo-key-one-param.yaml b/app/dts/bindings/behaviors/zmk,behavior-turbo-key-one-param.yaml new file mode 100644 index 00000000..3001ab55 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-turbo-key-one-param.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo key behavior + +compatible: "zmk,behavior-turbo-key-one-param" + +include: [one_param.yaml, turbo_base.yaml] diff --git a/app/dts/bindings/behaviors/zmk,behavior-turbo-key-two-param.yaml b/app/dts/bindings/behaviors/zmk,behavior-turbo-key-two-param.yaml new file mode 100644 index 00000000..15bbaae5 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-turbo-key-two-param.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo key behavior + +compatible: "zmk,behavior-turbo-key-two-param" + +include: [two_param.yaml, turbo_base.yaml] diff --git a/app/dts/bindings/behaviors/zmk,behavior-turbo-key.yaml b/app/dts/bindings/behaviors/zmk,behavior-turbo-key.yaml new file mode 100644 index 00000000..8130ac02 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-turbo-key.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo key behavior + +compatible: "zmk,behavior-turbo-key" + +include: [zero_param.yaml, turbo_base.yaml] diff --git a/app/dts/bindings/turbo/zmk,turbo-param-1to1.yaml b/app/dts/bindings/turbo/zmk,turbo-param-1to1.yaml new file mode 100644 index 00000000..082a8ac6 --- /dev/null +++ b/app/dts/bindings/turbo/zmk,turbo-param-1to1.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo Parameter One Substituted Into Next Binding's First Parameter + +compatible: "zmk,turbo-param-1to1" + +include: zero_param.yaml diff --git a/app/dts/bindings/turbo/zmk,turbo-param-1to2.yaml b/app/dts/bindings/turbo/zmk,turbo-param-1to2.yaml new file mode 100644 index 00000000..fa601a75 --- /dev/null +++ b/app/dts/bindings/turbo/zmk,turbo-param-1to2.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo Parameter One Substituted Into Next Binding's Second Parameter + +compatible: "zmk,turbo-param-1to2" + +include: zero_param.yaml diff --git a/app/dts/bindings/turbo/zmk,turbo-param-2to1.yaml b/app/dts/bindings/turbo/zmk,turbo-param-2to1.yaml new file mode 100644 index 00000000..9fc4c5ac --- /dev/null +++ b/app/dts/bindings/turbo/zmk,turbo-param-2to1.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo Parameter Two Substituted Into Next Binding's First Parameter + +compatible: "zmk,turbo-param-2to1" + +include: zero_param.yaml diff --git a/app/dts/bindings/turbo/zmk,turbo-param-2to2.yaml b/app/dts/bindings/turbo/zmk,turbo-param-2to2.yaml new file mode 100644 index 00000000..ceea8ed1 --- /dev/null +++ b/app/dts/bindings/turbo/zmk,turbo-param-2to2.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Turbo Parameter Two Substituted Into Next Binding's Second Parameter + +compatible: "zmk,turbo-param-2to2" + +include: zero_param.yaml diff --git a/app/src/behaviors/behavior_turbo_key.c b/app/src/behaviors/behavior_turbo_key.c new file mode 100644 index 00000000..65813c21 --- /dev/null +++ b/app/src/behaviors/behavior_turbo_key.c @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct behavior_turbo_data { + int32_t tap_ms; + int32_t wait_ms; + int32_t toggle_term_ms; + + uint32_t position; + bool is_active; + bool is_pressed; + + int32_t press_time; + + // Timer Data + bool timer_started; + bool timer_cancelled; + bool turbo_decided; + int64_t release_at; + struct k_work_delayable release_timer; + + uint32_t binding_count; + struct zmk_behavior_binding binding; + struct zmk_behavior_binding new_binding; + const struct zmk_behavior_binding bindings[]; +}; + +static int stop_timer(struct behavior_turbo_data *data) { + int timer_cancel_result = k_work_cancel_delayable(&data->release_timer); + if (timer_cancel_result == -EINPROGRESS) { + // too late to cancel, we'll let the timer handler clear up. + data->timer_cancelled = true; + } + return timer_cancel_result; +} + +static void clear_turbo(struct behavior_turbo_data *data) { + LOG_DBG("Turbo deactivated at position %d", data->position); + data->is_active = false; + stop_timer(data); +} + +static void reset_timer(struct behavior_turbo_data *data, struct zmk_behavior_binding_event event) { + data->release_at = event.timestamp + data->wait_ms; + int32_t ms_left = data->release_at - k_uptime_get(); + if (ms_left > 0) { + k_work_schedule(&data->release_timer, K_MSEC(ms_left)); + LOG_DBG("Successfully reset turbo timer at position %d", data->position); + } +} + +static void press_turbo_binding(struct zmk_behavior_binding_event *event, + const struct behavior_turbo_data *data) { + LOG_DBG("Pressing turbo binding %s, %d, %d", data->binding.behavior_dev, data->binding.param1, + data->binding.param2); + zmk_behavior_queue_add(event, data->binding, true, data->tap_ms); + zmk_behavior_queue_add(event, data->binding, false, 0); +} + +static void behavior_turbo_timer_handler(struct k_work *item) { + struct k_work_delayable *d_work = k_work_delayable_from_work(item); + + struct behavior_turbo_data *data = + CONTAINER_OF(d_work, struct behavior_turbo_data, release_timer); + + if (!data->is_active || data->timer_cancelled) { + return; + } + + LOG_DBG("Turbo timer reached."); + struct zmk_behavior_binding_event event = {.position = data->position, + .timestamp = k_uptime_get()}; + + press_turbo_binding(&event, data); + reset_timer(data, event); +} + +#define P1TO1 DEVICE_DT_NAME(DT_INST(0, zmk_turbo_param_1to1)) +#define P1TO2 DEVICE_DT_NAME(DT_INST(0, zmk_turbo_param_1to2)) +#define P2TO1 DEVICE_DT_NAME(DT_INST(0, zmk_turbo_param_2to1)) +#define P2TO2 DEVICE_DT_NAME(DT_INST(0, zmk_turbo_param_2to2)) + +#define ZM_IS_NODE_MATCH(a, b) (strcmp(a, b) == 0) + +#define IS_P1TO1(dev) ZM_IS_NODE_MATCH(dev, P1TO1) +#define IS_P1TO2(dev) ZM_IS_NODE_MATCH(dev, P1TO2) +#define IS_P2TO1(dev) ZM_IS_NODE_MATCH(dev, P2TO1) +#define IS_P2TO2(dev) ZM_IS_NODE_MATCH(dev, P2TO2) + +static bool handle_control_binding(struct behavior_turbo_data *data, + struct zmk_behavior_binding *binding, + const struct zmk_behavior_binding new_binding) { + if (IS_P1TO1(new_binding.behavior_dev)) { + data->new_binding.param1 = binding->param1; + LOG_DBG("turbo param: 1to1: %d", binding->param1); + } else if (IS_P1TO2(new_binding.behavior_dev)) { + data->new_binding.param2 = binding->param1; + LOG_DBG("turbo param: 1to2"); + } else if (IS_P2TO1(new_binding.behavior_dev)) { + data->new_binding.param1 = binding->param2; + LOG_DBG("turbo param: 2to1"); + } else if (IS_P2TO2(new_binding.behavior_dev)) { + data->new_binding.param2 = binding->param2; + LOG_DBG("turbo param: 2to2"); + } else { + return false; + } + + return true; +} + +static uint8_t get_binding_without_parameters_count(struct behavior_turbo_data *data) { + uint8_t bindings_without_parameters = 0; + + for (int i = 0; i < data->binding_count; i++) { + struct zmk_behavior_binding binding = data->bindings[i]; + if (!handle_control_binding(data, &binding, binding)) { + bindings_without_parameters++; + } + } + + return bindings_without_parameters; +} + +static void squash_params(struct behavior_turbo_data *data, struct zmk_behavior_binding *binding, + struct zmk_behavior_binding *new_bindings) { + uint8_t new_bindings_index = 0; + LOG_DBG("turbo bindings count is %d", data->binding_count); + + for (int i = 0; i < data->binding_count; i++) { + bool is_control_binding = handle_control_binding(data, binding, data->bindings[i]); + + if (!is_control_binding) { + data->new_binding.behavior_dev = data->bindings[i].behavior_dev; + + if (!data->new_binding.param1) { + data->new_binding.param1 = data->bindings[i].param1; + } + + if (!data->new_binding.param2) { + data->new_binding.param2 = data->bindings[i].param1; + } + + new_bindings[new_bindings_index] = data->new_binding; + new_bindings_index++; + } + + LOG_DBG("current turbo binding at index %d is %s, %d, %d", i, + data->new_binding.behavior_dev, data->new_binding.param1, data->new_binding.param2); + } +} + +static int on_turbo_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + struct behavior_turbo_data *data = dev->data; + + struct zmk_behavior_binding new_bindings[get_binding_without_parameters_count(data)]; + squash_params(data, binding, new_bindings); + + data->binding = new_bindings[0]; + + if (!data->is_active) { + data->is_active = true; + + LOG_DBG("Started new turbo at position %d", event.position); + + data->press_time = k_uptime_get(); + data->position = event.position; + + press_turbo_binding(&event, data); + reset_timer(data, event); + } else { + clear_turbo(data); + } + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_turbo_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + struct behavior_turbo_data *data = dev->data; + + if (data->is_active) { + data->is_pressed = false; + int32_t elapsedTime = k_uptime_get() - data->press_time; + LOG_DBG("turbo elapsed time: %d", elapsedTime); + if (elapsedTime > data->toggle_term_ms) { + clear_turbo(data); + } + } + return 0; +} + +static int behavior_turbo_key_init(const struct device *dev) { + struct behavior_turbo_data *data = dev->data; + k_work_init_delayable(&data->release_timer, behavior_turbo_timer_handler); + return 0; +}; + +#define TRANSFORMED_BEHAVIORS(n) \ + {LISTIFY(DT_PROP_LEN(n, bindings), ZMK_KEYMAP_EXTRACT_BINDING, (, ), n)} + +static const struct behavior_driver_api behavior_turbo_key_driver_api = { + .binding_pressed = on_turbo_binding_pressed, + .binding_released = on_turbo_binding_released, +}; + +#define TURBO_INST(n) \ + static struct behavior_turbo_data behavior_turbo_data_##n = { \ + .tap_ms = DT_PROP(n, tap_ms), \ + .wait_ms = DT_PROP(n, wait_ms), \ + .toggle_term_ms = DT_PROP(n, toggle_term_ms), \ + .bindings = TRANSFORMED_BEHAVIORS(n), \ + .binding_count = DT_PROP_LEN(n, bindings), \ + .is_active = false, \ + .is_pressed = false}; \ + BEHAVIOR_DT_DEFINE(n, behavior_turbo_key_init, NULL, &behavior_turbo_data_##n, NULL, \ + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_turbo_key_driver_api); + +DT_FOREACH_STATUS_OKAY(zmk_behavior_turbo_key, TURBO_INST) +DT_FOREACH_STATUS_OKAY(zmk_behavior_turbo_key_one_param, TURBO_INST) +DT_FOREACH_STATUS_OKAY(zmk_behavior_turbo_key_two_param, TURBO_INST) diff --git a/app/tests/turbo/basic/events.patterns b/app/tests/turbo/basic/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/turbo/basic/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/turbo/basic/keycode_events.snapshot b/app/tests/turbo/basic/keycode_events.snapshot new file mode 100644 index 00000000..d0767ca4 --- /dev/null +++ b/app/tests/turbo/basic/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 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 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 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/turbo/basic/native_posix_64.keymap b/app/tests/turbo/basic/native_posix_64.keymap new file mode 100644 index 00000000..ba4dd794 --- /dev/null +++ b/app/tests/turbo/basic/native_posix_64.keymap @@ -0,0 +1,8 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,1000) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/turbo/behavior_keymap.dtsi b/app/tests/turbo/behavior_keymap.dtsi new file mode 100644 index 00000000..e37d4f75 --- /dev/null +++ b/app/tests/turbo/behavior_keymap.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +/ { + behaviors { + turbo: turbo { + compatible = "zmk,behavior-turbo-key"; + label = "turbo"; + #binding-cells = <0>; + tap-ms = <5>; + wait-ms = <300>; + bindings = <&kp C>; + }; + t2: turbo2 { + compatible = "zmk,behavior-turbo-key"; + label = "turbo2"; + #binding-cells = <0>; + tap-ms = <5>; + wait-ms = <300>; + toggle-term-ms = <50>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &turbo &t2 + &kp D &kp Q>; + }; + }; +}; diff --git a/app/tests/turbo/toggle/events.patterns b/app/tests/turbo/toggle/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/turbo/toggle/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/turbo/toggle/keycode_events.snapshot b/app/tests/turbo/toggle/keycode_events.snapshot new file mode 100644 index 00000000..d0767ca4 --- /dev/null +++ b/app/tests/turbo/toggle/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 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 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 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/turbo/toggle/native_posix_64.keymap b/app/tests/turbo/toggle/native_posix_64.keymap new file mode 100644 index 00000000..c6745c8e --- /dev/null +++ b/app/tests/turbo/toggle/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,1000) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; diff --git a/docs/docs/keymaps/behaviors/turbo.md b/docs/docs/keymaps/behaviors/turbo.md new file mode 100644 index 00000000..bf771482 --- /dev/null +++ b/docs/docs/keymaps/behaviors/turbo.md @@ -0,0 +1,51 @@ +--- +title: Turbo Behavior +sidebar_label: Turbo Key +--- + +## Summary + +The turbo behavior will repeatedly trigger a behavior after a specified amount of time. + +### Configuration + +An example of how to implement a turbo key to output `A` every 5 seconds: + +``` +/ { + behaviors { + turbo_A: turbo_A { + compatible = "zmk,behavior-turbo-key"; + label = "TURBO_A"; + #binding-cells = <0>; + bindings = <&kp A>; + wait-ms = <5000>; + }; + }; +}; +``` + +### Behavior Binding + +- Reference: `&turbo_A` +- Parameter: None + +Example: + +``` +&turbo_A +``` + +### Advanced Configuration + +#### `wait-ms` + +Defines how often the behavior will trigger. Defaults to 200ms. + +#### `tap-ms` + +Defines how long the behavior will be held for each press. Defaults to 5ms. + +#### `toggle-term-ms` + +Releasing the turbo key within `toggle-term-ms` will toggle the repeating, removing the need to hold down the key. diff --git a/docs/sidebars.js b/docs/sidebars.js index 0a20a29e..4e58632e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -70,6 +70,7 @@ module.exports = { "keymaps/behaviors/mod-morph", "keymaps/behaviors/macros", "keymaps/behaviors/key-toggle", + "keymaps/behaviors/turbo", "keymaps/behaviors/sticky-key", "keymaps/behaviors/sticky-layer", "keymaps/behaviors/tap-dance",