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 8ba945d43..3b6cf7a6f 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