DEVELOPMENT ENVIRONMENT

~liljamo/nix-zmk

af11869259ad711b2c4ca23a5d26ec979e40531f — Jonni Liljamo 16 days ago 98b5b65
feat(lily58): turbo_a
3 files changed, 648 insertions(+), 1 deletions(-)

M config/lily58.keymap
M flake.nix
A patches/003-turbo-key-pr-1414.patch
M config/lily58.keymap => config/lily58.keymap +8 -1
@@ 25,6 25,13 @@
            bindings = <&kp ESC>, <&kp TILDE>;
            mods = <(MOD_LSFT|MOD_RSFT)>;
        };

        turbo_a: turbo_a {
            compatible = "zmk,behavior-turbo-key";
            #binding-cells = <0>;
            bindings = <&kp A>;
            wait-ms = <50>;
        };
    };

    keymap {


@@ 44,7 51,7 @@
            bindings = <
&bt BT_CLR  &bt BT_PRV  &bt BT_NXT  &none   &out OUT_BLE    &out OUT_USB                    &none   &none       &none       &none          &none    &none
&kp F1      &kp F2      &kp F3      &kp F4  &kp F5          &kp F6                          &kp F7  &kp F8      &kp F9      &kp F10        &kp F11  &kp F12
&none       &none       &none       &none   &none           &none                           &none   &kp INSERT  &kp HOME    &kp PAGE_UP    &none    &none
&none       &turbo_a    &none       &none   &none           &none                           &none   &kp INSERT  &kp HOME    &kp PAGE_UP    &none    &none
&none       &none       &none       &none   &none           &none           &none  &none    &none   &kp DELETE  &kp END     &kp PAGE_DOWN  &none    &none
                                    &none   &none           &none           &none  &none    &none &none       &none
            >;

M flake.nix => flake.nix +1 -0
@@ 26,6 26,7 @@

        postConfigure = ''
          patch -d ../zmk/ -p1 < ${./patches/001-lily58-pin.patch}
          patch -d ./zmk/ -p1 < ${./patches/003-turbo-key-pr-1414.patch}
        '';

        meta = {

A patches/003-turbo-key-pr-1414.patch => patches/003-turbo-key-pr-1414.patch +639 -0
@@ 0,0 1,639 @@
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 <behaviors/soft_off.dtsi>
 #include <behaviors/studio_unlock.dtsi>
 #include <behaviors/mouse_keys.dtsi>
+#include <behaviors/turbo.dtsi>
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 <zephyr/device.h>
+#include <drivers/behavior.h>
+#include <zephyr/logging/log.h>
+
+#include <zmk/behavior.h>
+#include <zmk/behavior_queue.h>
+#include <zmk/keymap.h>
+
+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 <dt-bindings/zmk/keys.h>
+#include <behaviors.dtsi>
+#include <dt-bindings/zmk/kscan_mock.h>
+
+/ {
+    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",