From 30c02dc99042ef74d6249bb40c46373b0adf0947 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Mon, 12 May 2025 18:58:26 +1000 Subject: [PATCH] =?UTF-8?q?feat(behaviors):=20Added=20=E2=80=98fire-hold-a?= =?UTF-8?q?s-tap=E2=80=99=20option=20for=20hold-tap=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new optional property `fire-hold-as-tap` to the hold-tap behavior. When enabled, if the hold condition is met, the hold behavior sends both a press and immediate release (simulating a tap) instead of sending the hold press on hold and release on key-up. This prevents OS key repeat behavior and allows safe assignment of normal keys (or other simple behaviors) to the hold action of hold-tap bindings. It is particularly useful for sending keyboard shortcuts on hold without unwanted repeats. The option is fully backwards compatible. It defaults to off. --- .../behaviors/zmk,behavior-hold-tap.yaml | 2 ++ app/src/behaviors/behavior_hold_tap.c | 16 ++++++++++++++++ docs/docs/keymaps/behaviors/hold-tap.mdx | 15 +++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml index 76f14d12d..ea1bd8732 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-hold-tap.yaml @@ -43,6 +43,8 @@ properties: type: boolean retro-tap: type: boolean + fire-hold-as-tap: + type: boolean hold-trigger-key-positions: type: array required: false diff --git a/app/src/behaviors/behavior_hold_tap.c b/app/src/behaviors/behavior_hold_tap.c index 3df3bc864..a5647e447 100644 --- a/app/src/behaviors/behavior_hold_tap.c +++ b/app/src/behaviors/behavior_hold_tap.c @@ -41,6 +41,7 @@ enum status { STATUS_TAP, STATUS_HOLD_INTERRUPT, STATUS_HOLD_TIMER, + STATUS_HOLD_TAP_SENT, }; enum decision_moment { @@ -62,6 +63,7 @@ struct behavior_hold_tap_config { bool hold_while_undecided; bool hold_while_undecided_linger; bool retro_tap; + bool fire_hold_as_tap; bool hold_trigger_on_release; int32_t hold_trigger_key_positions_len; int32_t hold_trigger_key_positions[]; @@ -379,6 +381,8 @@ static inline const char *status_str(enum status status) { return "hold-interrupt"; case STATUS_TAP: return "tap"; + case STATUS_HOLD_TAP_SENT: + return "hold-tap-sent"; default: return "UNKNOWN STATUS"; } @@ -467,6 +471,12 @@ static int press_binding(struct active_hold_tap *hold_tap) { if (hold_tap->config->hold_while_undecided) { // the hold is already active, so we don't need to press it again return 0; + } else if (hold_tap->config->fire_hold_as_tap) { + // send press followed immediately by release to simulate a tap + press_hold_binding(hold_tap); + release_hold_binding(hold_tap); + hold_tap->status = STATUS_HOLD_TAP_SENT; + return 0; } else { return press_hold_binding(hold_tap); } @@ -485,6 +495,11 @@ static int release_binding(struct active_hold_tap *hold_tap) { return 0; } + if (hold_tap->config->fire_hold_as_tap && hold_tap->status == STATUS_HOLD_TAP_SENT) { + // Already sent tap/release for hold + return 0; + } + if (hold_tap->status == STATUS_HOLD_TIMER || hold_tap->status == STATUS_HOLD_INTERRUPT) { return release_hold_binding(hold_tap); } else { @@ -870,6 +885,7 @@ static int behavior_hold_tap_init(const struct device *dev) { .hold_while_undecided = DT_INST_PROP(n, hold_while_undecided), \ .hold_while_undecided_linger = DT_INST_PROP(n, hold_while_undecided_linger), \ .retro_tap = DT_INST_PROP(n, retro_tap), \ + .fire_hold_as_tap = DT_INST_PROP(n, fire_hold_as_tap), \ .hold_trigger_on_release = DT_INST_PROP(n, hold_trigger_on_release), \ .hold_trigger_key_positions = DT_INST_PROP(n, hold_trigger_key_positions), \ .hold_trigger_key_positions_len = DT_INST_PROP_LEN(n, hold_trigger_key_positions), \ diff --git a/docs/docs/keymaps/behaviors/hold-tap.mdx b/docs/docs/keymaps/behaviors/hold-tap.mdx index 755230a29..7ade35456 100644 --- a/docs/docs/keymaps/behaviors/hold-tap.mdx +++ b/docs/docs/keymaps/behaviors/hold-tap.mdx @@ -460,3 +460,18 @@ For example, if you press `&mt LEFT_SHIFT A` and then release it without pressin retro-tap; }; ``` + +### `fire-hold-as-tap` + +If `fire-hold-as-tap` is enabled, the hold behavior will send both press and release immediately when the hold condition is met, instead of waiting for key release. +This prevents operating system key repeat behavior for hold actions that are intended to be momentary, such as triggering shortcut key combinations. + +For example, if you press and hold `&mt KC_PGUP KC_PGDN` with `fire-hold-as-tap` enabled, it will send a single `KC_PGUP` event when the hold condition triggers, and will not repeat or send additional events on key release. + +```dts +&mt { + fire-hold-as-tap; +}; +``` + +**Note:** This option has no effect on the tap behavior. It only changes the way the hold action is sent. \ No newline at end of file