diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e283487f..5fabf426 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -45,6 +45,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources(app PRIVATE src/behaviors/behavior_reset.c) target_sources(app PRIVATE src/behaviors/behavior_hold_tap.c) + target_sources(app PRIVATE src/behaviors/behavior_tap_dance.c) target_sources(app PRIVATE src/behaviors/behavior_sticky_key.c) target_sources(app PRIVATE src/behaviors/behavior_momentary_layer.c) target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c) diff --git a/app/dts/bindings/behaviors/zmk,behavior-tap-dance.yaml b/app/dts/bindings/behaviors/zmk,behavior-tap-dance.yaml new file mode 100644 index 00000000..42145be4 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-tap-dance.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Tap Dance Behavior + +compatible: "zmk,behavior-tap-dance" + +include: zero_param.yaml + +properties: + bindings: + type: phandle-array + required: true + tapping_term_ms: + type: int + default: 175 \ No newline at end of file diff --git a/app/src/behaviors/behavior_tap_dance.c b/app/src/behaviors/behavior_tap_dance.c new file mode 100644 index 00000000..2c8d9849 --- /dev/null +++ b/app/src/behaviors/behavior_tap_dance.c @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_tap_dance + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define ZMK_BHV_TAP_DANCE_MAX_HELD 10 + +#define ZMK_BHV_TAP_DANCE_POSITION_FREE ULONG_MAX + +struct behavior_tap_dance_config { + uint32_t tapping_term_ms; + int behavior_count; + struct zmk_behavior_binding *behaviors; +}; + +struct active_tap_dance { + // Tap Dance Data + int counter; + uint32_t position; + uint32_t param1; + uint32_t param2; + bool is_pressed; + const struct behavior_tap_dance_config *config; + // Timer Data + bool timer_started; + bool timer_cancelled; + bool tap_dance_decided; + int64_t release_at; + struct k_delayed_work release_timer; +}; + +struct active_tap_dance active_tap_dances[ZMK_BHV_TAP_DANCE_MAX_HELD] = {}; + +static struct active_tap_dance *store_tap_dance(uint32_t position, + const struct behavior_tap_dance_config *config) { + // Create a new tap dance instance + for (int i = 0; i < ZMK_BHV_TAP_DANCE_MAX_HELD; i++) { + struct active_tap_dance *const tap_dance = &active_tap_dances[i]; + if (tap_dance->position != ZMK_BHV_TAP_DANCE_POSITION_FREE || tap_dance->timer_cancelled) { + continue; + } + tap_dance->counter = 1; + tap_dance->position = position; + tap_dance->config = config; + tap_dance->release_at = 0; + tap_dance->is_pressed = true; + tap_dance->timer_started = true; + tap_dance->timer_cancelled = false; + tap_dance->tap_dance_decided = false; + return tap_dance; + } + return NULL; +} + +static struct active_tap_dance *find_tap_dance(uint32_t position) { + // Search for existing tap dances at 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) { + return &active_tap_dances[i]; + } + } + return NULL; +} + +static int stop_timer(struct active_tap_dance *tap_dance) { + int timer_cancel_result = k_delayed_work_cancel(&tap_dance->release_timer); + if (timer_cancel_result == -EINPROGRESS) { + // too late to cancel, we'll let the timer handler clear up. + tap_dance->timer_cancelled = true; + } + return timer_cancel_result; +} + +static void clear_tap_dance(struct active_tap_dance *tap_dance) { + // FREE position, reset counter. + tap_dance->position = ZMK_BHV_TAP_DANCE_POSITION_FREE; + tap_dance->counter = 1; +} + +static void reset_timer(struct active_tap_dance *tap_dance, + struct zmk_behavior_binding_event event) { + // Start the timer if one hasn't started already. Reset it if one exists. + tap_dance->release_at = event.timestamp + tap_dance->config->tapping_term_ms; + int32_t ms_left = tap_dance->release_at - k_uptime_get(); + if (ms_left > 0) { + k_delayed_work_submit(&tap_dance->release_timer, K_MSEC(ms_left)); + } +} + +static inline int press_tap_dance_behavior(struct active_tap_dance *tap_dance, int64_t timestamp) { + // Press tap dance binding + struct zmk_behavior_binding binding = + (tap_dance->counter < tap_dance->config->behavior_count) + ? tap_dance->config->behaviors[(tap_dance->counter) - 1] + : tap_dance->config->behaviors[(tap_dance->config->behavior_count) - 1]; + struct zmk_behavior_binding_event event = { + .position = tap_dance->position, + .timestamp = timestamp, + }; + behavior_keymap_binding_pressed(&binding, event); + return 0; +} + +static inline int release_tap_dance_behavior(struct active_tap_dance *tap_dance, + int64_t timestamp) { + // Release tap dance binding + struct zmk_behavior_binding binding = + (tap_dance->counter < tap_dance->config->behavior_count) + ? tap_dance->config->behaviors[(tap_dance->counter) - 1] + : tap_dance->config->behaviors[(tap_dance->config->behavior_count) - 1]; + struct zmk_behavior_binding_event event = { + .position = tap_dance->position, + .timestamp = timestamp, + }; + behavior_keymap_binding_released(&binding, event); + clear_tap_dance(tap_dance); + return 0; +} + +static int on_tap_dance_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + // Code executes when the tap dance key is pressed. + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_tap_dance_config *cfg = dev->config; + struct active_tap_dance *tap_dance; + tap_dance = find_tap_dance(event.position); + // No active tap dance found on keypress location, create a new one + if (tap_dance == NULL) { + tap_dance = store_tap_dance(event.position, cfg); + LOG_DBG("%d creating new tap dance", event.position); + reset_timer(tap_dance, event); + return ZMK_BEHAVIOR_OPAQUE; + } + // An active tap dance was found at the keypress location. Stop the timer. + tap_dance->is_pressed = true; + LOG_DBG("%d tap dance re-pressed", event.position); + stop_timer(tap_dance); + // Increment the counter. Check if the counter has reached the maximum behavior number. + tap_dance->counter++; + if (tap_dance->counter >= cfg->behavior_count) { + // Counter is greater than or equal to the maximum number of behaviors. + // Resolve tap dance immediately. + press_tap_dance_behavior(tap_dance, event.timestamp); + tap_dance->tap_dance_decided = true; + return ZMK_BEHAVIOR_OPAQUE; + } + // Counter is less than maximum number of behaviors, reset timer. + reset_timer(tap_dance, event); + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_tap_dance_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + // Code executes when the tap dance key is released. + LOG_DBG("%d tap dance keybind released", event.position); + struct active_tap_dance *tap_dance = find_tap_dance(event.position); + if (tap_dance == NULL) { + LOG_ERR("ACTIVE TAP DANCE CLEARED TOO EARLY"); + return ZMK_BEHAVIOR_OPAQUE; + } + tap_dance->is_pressed = false; + // An active tap dance has already been decided and is registering keypresses. + // Release tap dance on key-release. + if (tap_dance->tap_dance_decided) { + LOG_DBG("Maximum count reached and/or key is held! Counter reached: %d", + tap_dance->counter); + release_tap_dance_behavior(tap_dance, event.timestamp); + tap_dance->tap_dance_decided = false; + } + return ZMK_BEHAVIOR_OPAQUE; +} + +void behavior_tap_dance_timer_handler(struct k_work *item) { + LOG_DBG("Timer Handler executes"); + // Timer for tap dance has ran out. + struct active_tap_dance *tap_dance = CONTAINER_OF(item, struct active_tap_dance, release_timer); + if (tap_dance->position == ZMK_BHV_TAP_DANCE_POSITION_FREE) { + return; + } + tap_dance->timer_cancelled = false; + press_tap_dance_behavior(tap_dance, tap_dance->release_at); + // Tap dance is held, store that information to prepare for key release + if (tap_dance->is_pressed) { + tap_dance->tap_dance_decided = true; + return; + } + // Tap dance is not held. Release the tap dance. + LOG_DBG("Tap dance releases immediately! Counter reached: %d", tap_dance->counter); + release_tap_dance_behavior(tap_dance, tap_dance->release_at); + return; +} + +static const struct behavior_driver_api behavior_tap_dance_driver_api = { + .binding_pressed = on_tap_dance_binding_pressed, + .binding_released = on_tap_dance_binding_released, +}; + +static int tap_dance_position_state_changed_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_tap_dance, tap_dance_position_state_changed_listener); +ZMK_SUBSCRIPTION(behavior_tap_dance, zmk_position_state_changed); + +static int tap_dance_position_state_changed_listener(const zmk_event_t *eh) { + // A keypress with a different position than the last tap dance has been recorded. + struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); + if (ev == NULL) { + return 0; + } + 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) { + continue; + } + if (tap_dance->position != ev->position && tap_dance->timer_started) { + // Resolve tap dance immediately before new keypress. + LOG_DBG("Tap dance interrupted, position changed."); + stop_timer(tap_dance); + press_tap_dance_behavior(tap_dance, tap_dance->release_at); + release_tap_dance_behavior(tap_dance, tap_dance->release_at); + } + } + return ZMK_EV_EVENT_BUBBLE; +} + +#define _TRANSFORM_ENTRY(idx, node) \ + { \ + .behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(node, bindings, idx)), \ + .param1 = COND_CODE_0(DT_INST_PHA_HAS_CELL_AT_IDX(node, bindings, idx, param1), (0), \ + (DT_INST_PHA_BY_IDX(node, bindings, idx, param1))), \ + .param2 = COND_CODE_0(DT_INST_PHA_HAS_CELL_AT_IDX(node, bindings, idx, param2), (0), \ + (DT_INST_PHA_BY_IDX(node, bindings, idx, param2))), \ + }, + +#define TRANSFORMED_BINDINGS(node) \ + { UTIL_LISTIFY(DT_INST_PROP_LEN(node, bindings), _TRANSFORM_ENTRY, node) } + +static int behavior_tap_dance_init(const struct device *dev) { + static bool init_first_run = true; + if (init_first_run) { + for (int i = 0; i < ZMK_BHV_TAP_DANCE_MAX_HELD; i++) { + k_delayed_work_init(&active_tap_dances[i].release_timer, + behavior_tap_dance_timer_handler); + active_tap_dances[i].position = ZMK_BHV_TAP_DANCE_POSITION_FREE; + } + } + init_first_run = false; + return 0; +} + +struct behavior_tap_dance_data {}; +static struct behavior_tap_dance_data behavior_tap_dance_data; + +#define KP_INST(n) \ + static struct zmk_behavior_binding \ + behavior_tap_dance_config_##n##_bindings[DT_INST_PROP_LEN(n, bindings)] = \ + TRANSFORMED_BINDINGS(n); \ + static struct behavior_tap_dance_config behavior_tap_dance_config_##n = { \ + .tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \ + .behaviors = behavior_tap_dance_config_##n##_bindings, \ + .behavior_count = DT_INST_PROP_LEN(n, bindings)}; \ + DEVICE_AND_API_INIT(behavior_tap_dance_##n, DT_INST_LABEL(n), behavior_tap_dance_init, \ + &behavior_tap_dance_data, &behavior_tap_dance_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_tap_dance_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif \ No newline at end of file diff --git a/app/tests/tap-dance/1a-dn-up/events.patterns b/app/tests/tap-dance/1a-dn-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/1a-dn-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/1a-dn-up/keycode_events.snapshot b/app/tests/tap-dance/1a-dn-up/keycode_events.snapshot new file mode 100644 index 00000000..0a8dd191 --- /dev/null +++ b/app/tests/tap-dance/1a-dn-up/keycode_events.snapshot @@ -0,0 +1,6 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_timer_handler: Timer Handler executes +kp_pressed: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 +td_timer_handler: Tap dance releases immediately! Counter reached: 1 +kp_released: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/1a-dn-up/native_posix.keymap b/app/tests/tap-dance/1a-dn-up/native_posix.keymap new file mode 100644 index 00000000..e13e74e6 --- /dev/null +++ b/app/tests/tap-dance/1a-dn-up/native_posix.keymap @@ -0,0 +1,11 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,200) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/1b-dn-up-dn-up/events.patterns b/app/tests/tap-dance/1b-dn-up-dn-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/1b-dn-up-dn-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/1b-dn-up-dn-up/keycode_events.snapshot b/app/tests/tap-dance/1b-dn-up-dn-up/keycode_events.snapshot new file mode 100644 index 00000000..b7e33ed0 --- /dev/null +++ b/app/tests/tap-dance/1b-dn-up-dn-up/keycode_events.snapshot @@ -0,0 +1,8 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +td_binding_released: 0 tap dance keybind released +td_timer_handler: Timer Handler executes +kp_pressed: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 +td_timer_handler: Tap dance releases immediately! Counter reached: 2 +kp_released: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/1b-dn-up-dn-up/native_posix.keymap b/app/tests/tap-dance/1b-dn-up-dn-up/native_posix.keymap new file mode 100644 index 00000000..b995fae4 --- /dev/null +++ b/app/tests/tap-dance/1b-dn-up-dn-up/native_posix.keymap @@ -0,0 +1,13 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,200) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/events.patterns b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/keycode_events.snapshot b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/keycode_events.snapshot new file mode 100644 index 00000000..0625a575 --- /dev/null +++ b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/keycode_events.snapshot @@ -0,0 +1,9 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +kp_pressed: usage_page 0x07 keycode 0x20 implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +td_binding_released: Maximum count reached and/or key is held! Counter reached: 3 +kp_released: usage_page 0x07 keycode 0x20 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/native_posix.keymap b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/native_posix.keymap new file mode 100644 index 00000000..92bd5283 --- /dev/null +++ b/app/tests/tap-dance/1c-dn-up-dn-up-dn-up/native_posix.keymap @@ -0,0 +1,15 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,200) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/2a-dn-hold-up/events.patterns b/app/tests/tap-dance/2a-dn-hold-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/2a-dn-hold-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/2a-dn-hold-up/keycode_events.snapshot b/app/tests/tap-dance/2a-dn-hold-up/keycode_events.snapshot new file mode 100644 index 00000000..fd14d9e4 --- /dev/null +++ b/app/tests/tap-dance/2a-dn-hold-up/keycode_events.snapshot @@ -0,0 +1,6 @@ +td_binding_pressed: 0 creating new tap dance +td_timer_handler: Timer Handler executes +kp_pressed: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +td_binding_released: Maximum count reached and/or key is held! Counter reached: 1 +kp_released: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/2a-dn-hold-up/native_posix.keymap b/app/tests/tap-dance/2a-dn-hold-up/native_posix.keymap new file mode 100644 index 00000000..8fd406c3 --- /dev/null +++ b/app/tests/tap-dance/2a-dn-hold-up/native_posix.keymap @@ -0,0 +1,11 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,200) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/2b-dn-up-dn-hold-up/events.patterns b/app/tests/tap-dance/2b-dn-up-dn-hold-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/2b-dn-up-dn-hold-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/2b-dn-up-dn-hold-up/keycode_events.snapshot b/app/tests/tap-dance/2b-dn-up-dn-hold-up/keycode_events.snapshot new file mode 100644 index 00000000..fcca90e9 --- /dev/null +++ b/app/tests/tap-dance/2b-dn-up-dn-hold-up/keycode_events.snapshot @@ -0,0 +1,8 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +td_timer_handler: Timer Handler executes +kp_pressed: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +td_binding_released: Maximum count reached and/or key is held! Counter reached: 2 +kp_released: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/2b-dn-up-dn-hold-up/native_posix.keymap b/app/tests/tap-dance/2b-dn-up-dn-hold-up/native_posix.keymap new file mode 100644 index 00000000..035b2fb2 --- /dev/null +++ b/app/tests/tap-dance/2b-dn-up-dn-hold-up/native_posix.keymap @@ -0,0 +1,13 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,200) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/events.patterns b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/keycode_events.snapshot b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/keycode_events.snapshot new file mode 100644 index 00000000..0625a575 --- /dev/null +++ b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/keycode_events.snapshot @@ -0,0 +1,9 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +kp_pressed: usage_page 0x07 keycode 0x20 implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +td_binding_released: Maximum count reached and/or key is held! Counter reached: 3 +kp_released: usage_page 0x07 keycode 0x20 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/native_posix.keymap b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/native_posix.keymap new file mode 100644 index 00000000..267e7b73 --- /dev/null +++ b/app/tests/tap-dance/2c-dn-up-dn-up-dn-hold-up/native_posix.keymap @@ -0,0 +1,15 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,200) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/3a-dn-kcdn-up-kcup/events.patterns b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/3a-dn-kcdn-up-kcup/keycode_events.snapshot b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/keycode_events.snapshot new file mode 100644 index 00000000..819a579f --- /dev/null +++ b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/keycode_events.snapshot @@ -0,0 +1,7 @@ +td_binding_pressed: 0 creating new tap dance +position_changed: Tap dance interrupted, position changed. +kp_pressed: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x1e implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x27 implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +kp_released: usage_page 0x07 keycode 0x27 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/3a-dn-kcdn-up-kcup/native_posix.keymap b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/native_posix.keymap new file mode 100644 index 00000000..ced6642c --- /dev/null +++ b/app/tests/tap-dance/3a-dn-kcdn-up-kcup/native_posix.keymap @@ -0,0 +1,13 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/events.patterns b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/events.patterns new file mode 100644 index 00000000..65642958 --- /dev/null +++ b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/events.patterns @@ -0,0 +1,4 @@ +s/.*hid_listener_keycode/kp/p +s/.*on_tap_dance_binding/td_binding/p +s/.*behavior_tap_dance_timer/td_timer/p +s/.*tap_dance_position_state_changed_listener/position_changed/p \ No newline at end of file diff --git a/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/keycode_events.snapshot b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/keycode_events.snapshot new file mode 100644 index 00000000..af679441 --- /dev/null +++ b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/keycode_events.snapshot @@ -0,0 +1,9 @@ +td_binding_pressed: 0 creating new tap dance +td_binding_released: 0 tap dance keybind released +td_binding_pressed: 0 tap dance re-pressed +position_changed: Tap dance interrupted, position changed. +kp_pressed: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x1f implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x27 implicit_mods 0x00 explicit_mods 0x00 +td_binding_released: 0 tap dance keybind released +kp_released: usage_page 0x07 keycode 0x27 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/native_posix.keymap b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/native_posix.keymap new file mode 100644 index 00000000..849f6abc --- /dev/null +++ b/app/tests/tap-dance/3b-dn-up-dn-kcdn-up-kcup/native_posix.keymap @@ -0,0 +1,15 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/tap-dance/behavior_keymap.dtsi b/app/tests/tap-dance/behavior_keymap.dtsi new file mode 100644 index 00000000..ca70c544 --- /dev/null +++ b/app/tests/tap-dance/behavior_keymap.dtsi @@ -0,0 +1,27 @@ +#include +#include +#include + +/ { + behaviors { + + tdb: tap_dance_basic { + compatible = "zmk,behavior-tap-dance"; + label = "TAP_DANCE_BASIC"; + #binding-cells = <0>; + tapping_term_ms = <200>; + bindings = <&kp N1>, <&kp N2>, <&kp N3>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &tdb &kp N0 + &kp N0 &kp N0>; + }; + }; +}; diff --git a/docs/docs/behaviors/tap-dance.md b/docs/docs/behaviors/tap-dance.md new file mode 100644 index 00000000..fc8bca57 --- /dev/null +++ b/docs/docs/behaviors/tap-dance.md @@ -0,0 +1,52 @@ +--- +title: Tap Dance Behavior +sidebar_label: Tap Dance +--- + +## Summary + +A tap dance key outputs a keycode or behavior corresponding to how many times it is pressed. +Tap dances are completely custom, so for every unique tap dance key, a new tap dance must be defined in your keymap's +`behaviors`. + +### Configuration + +#### `tapping-term-ms` + +Defines how much time in milliseconds after the tap dance is pressed before a keybind is registered. + +#### `bindings` + +A list of one or more keybinds. This list can include [any keycode in ZMK](../codes/) and keybinds for ZMK behaviors. + +#### Example Usage + +This example configures a tap dance that outputs the number of keypresses from 1-5: + +``` +#include +#include + +/ { + behaviors { + td: tap_dance { + compatible = "zmk,behavior-tap-dance"; + label = "TAP_TEST"; + #binding-cells = <0>; + tapping_term_ms = <1000>; + bindings = <&kp N1>, <&kp N2>, <&kp N3>, <&kp N4> , <&kp N5>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &td + >; + }; + }; +}; + +```