behaviors(hold-tap): Implement quick_tap_ms (TAPPING_FORCE_HOLD)
Tap-and-hold a hold-tap to hold the tap behavior so it can repeat. After a tap, if the same key is pressed within `quick_tap_ms`, the tap behavior is always picked. This is useful for things like `&ht LSHFT BACKSPACE` where holding the backspace is required. Implements #288.
This commit is contained in:
parent
6c7ab0ce53
commit
9a7908b632
15 changed files with 142 additions and 1 deletions
|
@ -13,6 +13,9 @@ properties:
|
||||||
required: true
|
required: true
|
||||||
tapping_term_ms:
|
tapping_term_ms:
|
||||||
type: int
|
type: int
|
||||||
|
quick_tap_ms:
|
||||||
|
type: int
|
||||||
|
default: -1
|
||||||
flavor:
|
flavor:
|
||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
|
|
|
@ -40,6 +40,7 @@ struct behavior_hold_tap_config {
|
||||||
int tapping_term_ms;
|
int tapping_term_ms;
|
||||||
char *hold_behavior_dev;
|
char *hold_behavior_dev;
|
||||||
char *tap_behavior_dev;
|
char *tap_behavior_dev;
|
||||||
|
int quick_tap_ms;
|
||||||
enum flavor flavor;
|
enum flavor flavor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,6 +68,24 @@ struct active_hold_tap active_hold_taps[ZMK_BHV_HOLD_TAP_MAX_HELD] = {};
|
||||||
// We capture most position_state_changed events and some modifiers_state_changed events.
|
// We capture most position_state_changed events and some modifiers_state_changed events.
|
||||||
const zmk_event_t *captured_events[ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS] = {};
|
const zmk_event_t *captured_events[ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS] = {};
|
||||||
|
|
||||||
|
// Keep track of which key was tapped most recently for 'quick_tap_ms'
|
||||||
|
struct last_tapped {
|
||||||
|
int32_t position;
|
||||||
|
int64_t tap_deadline;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct last_tapped last_tapped;
|
||||||
|
|
||||||
|
static void store_last_tapped(struct active_hold_tap *hold_tap) {
|
||||||
|
last_tapped.position = hold_tap->position;
|
||||||
|
last_tapped.tap_deadline = hold_tap->timestamp + hold_tap->config->quick_tap_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_quick_tap(struct active_hold_tap *hold_tap) {
|
||||||
|
return last_tapped.position == hold_tap->position &&
|
||||||
|
last_tapped.tap_deadline > hold_tap->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
static int capture_event(const zmk_event_t *event) {
|
static int capture_event(const zmk_event_t *event) {
|
||||||
for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) {
|
for (int i = 0; i < ZMK_BHV_HOLD_TAP_MAX_CAPTURED_EVENTS; i++) {
|
||||||
if (captured_events[i] == NULL) {
|
if (captured_events[i] == NULL) {
|
||||||
|
@ -191,6 +210,7 @@ enum decision_moment {
|
||||||
HT_OTHER_KEY_DOWN = 1,
|
HT_OTHER_KEY_DOWN = 1,
|
||||||
HT_OTHER_KEY_UP = 2,
|
HT_OTHER_KEY_UP = 2,
|
||||||
HT_TIMER_EVENT = 3,
|
HT_TIMER_EVENT = 3,
|
||||||
|
HT_QUICK_TAP = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_moment event) {
|
static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_moment event) {
|
||||||
|
@ -204,6 +224,10 @@ static void decide_balanced(struct active_hold_tap *hold_tap, enum decision_mome
|
||||||
hold_tap->is_hold = 1;
|
hold_tap->is_hold = 1;
|
||||||
hold_tap->is_decided = true;
|
hold_tap->is_decided = true;
|
||||||
break;
|
break;
|
||||||
|
case HT_QUICK_TAP:
|
||||||
|
hold_tap->is_hold = 0;
|
||||||
|
hold_tap->is_decided = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -219,6 +243,10 @@ static void decide_tap_preferred(struct active_hold_tap *hold_tap, enum decision
|
||||||
hold_tap->is_hold = 1;
|
hold_tap->is_hold = 1;
|
||||||
hold_tap->is_decided = true;
|
hold_tap->is_decided = true;
|
||||||
break;
|
break;
|
||||||
|
case HT_QUICK_TAP:
|
||||||
|
hold_tap->is_hold = 0;
|
||||||
|
hold_tap->is_decided = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -235,6 +263,10 @@ static void decide_hold_preferred(struct active_hold_tap *hold_tap, enum decisio
|
||||||
hold_tap->is_hold = 1;
|
hold_tap->is_hold = 1;
|
||||||
hold_tap->is_decided = true;
|
hold_tap->is_decided = true;
|
||||||
break;
|
break;
|
||||||
|
case HT_QUICK_TAP:
|
||||||
|
hold_tap->is_hold = 0;
|
||||||
|
hold_tap->is_decided = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -293,6 +325,7 @@ static void decide_hold_tap(struct active_hold_tap *hold_tap, enum decision_mome
|
||||||
binding.behavior_dev = hold_tap->config->tap_behavior_dev;
|
binding.behavior_dev = hold_tap->config->tap_behavior_dev;
|
||||||
binding.param1 = hold_tap->param_tap;
|
binding.param1 = hold_tap->param_tap;
|
||||||
binding.param2 = 0;
|
binding.param2 = 0;
|
||||||
|
store_last_tapped(hold_tap);
|
||||||
}
|
}
|
||||||
behavior_keymap_binding_pressed(&binding, event);
|
behavior_keymap_binding_pressed(&binding, event);
|
||||||
release_captured_events();
|
release_captured_events();
|
||||||
|
@ -320,6 +353,10 @@ static int on_hold_tap_binding_pressed(struct zmk_behavior_binding *binding,
|
||||||
LOG_DBG("%d new undecided hold_tap", event.position);
|
LOG_DBG("%d new undecided hold_tap", event.position);
|
||||||
undecided_hold_tap = hold_tap;
|
undecided_hold_tap = hold_tap;
|
||||||
|
|
||||||
|
if (is_quick_tap(hold_tap)) {
|
||||||
|
decide_hold_tap(hold_tap, HT_QUICK_TAP);
|
||||||
|
}
|
||||||
|
|
||||||
// if this behavior was queued we have to adjust the timer to only
|
// if this behavior was queued we have to adjust the timer to only
|
||||||
// wait for the remaining time.
|
// wait for the remaining time.
|
||||||
int32_t tapping_term_ms_left = (hold_tap->timestamp + cfg->tapping_term_ms) - k_uptime_get();
|
int32_t tapping_term_ms_left = (hold_tap->timestamp + cfg->tapping_term_ms) - k_uptime_get();
|
||||||
|
@ -492,6 +529,7 @@ static struct behavior_hold_tap_data behavior_hold_tap_data;
|
||||||
.tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \
|
.tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \
|
||||||
.hold_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 0)), \
|
.hold_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 0)), \
|
||||||
.tap_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 1)), \
|
.tap_behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(n, bindings, 1)), \
|
||||||
|
.quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \
|
||||||
.flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \
|
.flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \
|
||||||
}; \
|
}; \
|
||||||
DEVICE_AND_API_INIT(behavior_hold_tap_##n, DT_INST_LABEL(n), behavior_hold_tap_init, \
|
DEVICE_AND_API_INIT(behavior_hold_tap_##n, DT_INST_LABEL(n), behavior_hold_tap_init, \
|
||||||
|
|
4
app/tests/hold-tap/balanced/5-quick-tap/events.patterns
Normal file
4
app/tests/hold-tap/balanced/5-quick-tap/events.patterns
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
s/.*hid_listener_keycode/kp/p
|
||||||
|
s/.*mo_keymap_binding/mo/p
|
||||||
|
s/.*on_hold_tap_binding/ht_binding/p
|
||||||
|
s/.*decide_hold_tap/ht_decide/p
|
|
@ -0,0 +1,10 @@
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (balanced event 0)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (balanced event 4)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
14
app/tests/hold-tap/balanced/5-quick-tap/native_posix.keymap
Normal file
14
app/tests/hold-tap/balanced/5-quick-tap/native_posix.keymap
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#include <dt-bindings/zmk/keys.h>
|
||||||
|
#include <behaviors.dtsi>
|
||||||
|
#include <dt-bindings/zmk/kscan_mock.h>
|
||||||
|
#include "../behavior_keymap.dtsi"
|
||||||
|
|
||||||
|
|
||||||
|
&kscan {
|
||||||
|
events = <
|
||||||
|
ZMK_MOCK_PRESS(0,0,10)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
ZMK_MOCK_PRESS(0,0,400)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
>;
|
||||||
|
};
|
|
@ -10,6 +10,7 @@
|
||||||
#binding-cells = <2>;
|
#binding-cells = <2>;
|
||||||
flavor = "balanced";
|
flavor = "balanced";
|
||||||
tapping_term_ms = <300>;
|
tapping_term_ms = <300>;
|
||||||
|
quick_tap_ms = <200>;
|
||||||
bindings = <&kp>, <&kp>;
|
bindings = <&kp>, <&kp>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
s/.*hid_listener_keycode/kp/p
|
||||||
|
s/.*mo_keymap_binding/mo/p
|
||||||
|
s/.*on_hold_tap_binding/ht_binding/p
|
||||||
|
s/.*decide_hold_tap/ht_decide/p
|
|
@ -0,0 +1,10 @@
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (hold-preferred event 0)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (hold-preferred event 4)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include <dt-bindings/zmk/keys.h>
|
||||||
|
#include <behaviors.dtsi>
|
||||||
|
#include <dt-bindings/zmk/kscan_mock.h>
|
||||||
|
#include "../behavior_keymap.dtsi"
|
||||||
|
|
||||||
|
|
||||||
|
&kscan {
|
||||||
|
events = <
|
||||||
|
ZMK_MOCK_PRESS(0,0,10)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
ZMK_MOCK_PRESS(0,0,400)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
>;
|
||||||
|
};
|
|
@ -12,6 +12,7 @@
|
||||||
#binding-cells = <2>;
|
#binding-cells = <2>;
|
||||||
flavor = "hold-preferred";
|
flavor = "hold-preferred";
|
||||||
tapping_term_ms = <300>;
|
tapping_term_ms = <300>;
|
||||||
|
quick_tap_ms = <200>;
|
||||||
bindings = <&kp>, <&kp>;
|
bindings = <&kp>, <&kp>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
s/.*hid_listener_keycode/kp/p
|
||||||
|
s/.*mo_keymap_binding/mo/p
|
||||||
|
s/.*on_hold_tap_binding/ht_binding/p
|
||||||
|
s/.*decide_hold_tap/ht_decide/p
|
|
@ -0,0 +1,10 @@
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (tap-preferred event 0)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
||||||
|
ht_binding_pressed: 0 new undecided hold_tap
|
||||||
|
ht_decide: 0 decided tap (tap-preferred event 4)
|
||||||
|
kp_pressed: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
kp_released: usage_page 0x07 keycode 0x09 implicit_mods 0x00 explicit_mods 0x00
|
||||||
|
ht_binding_released: 0 cleaning up hold-tap
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include <dt-bindings/zmk/keys.h>
|
||||||
|
#include <behaviors.dtsi>
|
||||||
|
#include <dt-bindings/zmk/kscan_mock.h>
|
||||||
|
#include "../behavior_keymap.dtsi"
|
||||||
|
|
||||||
|
|
||||||
|
&kscan {
|
||||||
|
events = <
|
||||||
|
ZMK_MOCK_PRESS(0,0,10)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
ZMK_MOCK_PRESS(0,0,400)
|
||||||
|
ZMK_MOCK_RELEASE(0,0,10)
|
||||||
|
>;
|
||||||
|
};
|
|
@ -10,6 +10,7 @@
|
||||||
#binding-cells = <2>;
|
#binding-cells = <2>;
|
||||||
flavor = "tap-preferred";
|
flavor = "tap-preferred";
|
||||||
tapping_term_ms = <300>;
|
tapping_term_ms = <300>;
|
||||||
|
quick_tap_ms = <200>;
|
||||||
bindings = <&kp>, <&kp>;
|
bindings = <&kp>, <&kp>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ Simply put, the hold-tap key will output the 'hold' behavior if it's held for a
|
||||||
|
|
||||||
### Hold-Tap
|
### Hold-Tap
|
||||||
|
|
||||||
The `tapping_term_ms` parameter decides between a 'tap' and a 'hold'.
|
The graph below shows how the hold-tap decides between a 'tap' and a 'hold'.
|
||||||
|
|
||||||
![Simple behavior](../assets/hold-tap/case1_2.png)
|
![Simple behavior](../assets/hold-tap/case1_2.png)
|
||||||
|
|
||||||
|
@ -37,6 +37,18 @@ For basic usage, please see [mod-tap](./mod-tap.md) and [layer-tap](./layers.md)
|
||||||
|
|
||||||
### Advanced Configuration
|
### Advanced Configuration
|
||||||
|
|
||||||
|
#### `tapping_term_ms`
|
||||||
|
|
||||||
|
Defines how long a key must be pressed to trigger Hold behavior.
|
||||||
|
|
||||||
|
#### `quick_tap_ms`
|
||||||
|
|
||||||
|
If you press a tapped hold-tap again within `quick_tap_ms` milliseconds, it will always trigger the tap behavior. This is useful for things like a backspace, where a quick tap+hold holds backspace pressed. Set this to a negative value to disable. The default is -1 (disabled).
|
||||||
|
|
||||||
|
In QMK, unlike ZMK, this functionality is enabled by default, and you turn it off using `TAPPING_FORCE_HOLD`.
|
||||||
|
|
||||||
|
#### Home row mods
|
||||||
|
|
||||||
This example configures a hold-tap that works well for homerow mods:
|
This example configures a hold-tap that works well for homerow mods:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -50,6 +62,7 @@ This example configures a hold-tap that works well for homerow mods:
|
||||||
label = "HOMEROW_MODS";
|
label = "HOMEROW_MODS";
|
||||||
#binding-cells = <2>;
|
#binding-cells = <2>;
|
||||||
tapping_term_ms = <150>;
|
tapping_term_ms = <150>;
|
||||||
|
quick_tap_ms = <0>;
|
||||||
flavor = "tap-preferred";
|
flavor = "tap-preferred";
|
||||||
bindings = <&kp>, <&kp>;
|
bindings = <&kp>, <&kp>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue