feat(behaviors): sticky keys no longer sticky after being held

Sticky keys that are pressed down for longer than
`no-sticky-after-hold-ms` will immediately deactivate upon release. If
the property `no-sticky-after-hold-ms` does not exist, the value of
`release-after-ms` will be used as a fallback.
This commit is contained in:
down 2023-11-22 14:35:53 +07:00
parent 0a4b1a6533
commit 95185e384d
9 changed files with 142 additions and 0 deletions

View file

@ -14,6 +14,8 @@ properties:
release-after-ms:
type: int
required: true
no-sticky-after-hold-ms:
type: int
quick-release:
type: boolean
ignore-modifiers:

View file

@ -30,6 +30,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
struct behavior_sticky_key_config {
uint32_t release_after_ms;
uint32_t no_sticky_after_hold_ms;
bool quick_release;
bool ignore_modifiers;
struct zmk_behavior_binding behavior;
@ -41,6 +42,7 @@ struct active_sticky_key {
uint32_t param2;
const struct behavior_sticky_key_config *config;
// timer data.
int64_t no_sticky_after_hold_at;
bool timer_started;
bool timer_cancelled;
int64_t release_at;
@ -66,6 +68,7 @@ static struct active_sticky_key *store_sticky_key(uint32_t position, uint32_t pa
sticky_key->param2 = param2;
sticky_key->config = config;
sticky_key->release_at = 0;
sticky_key->no_sticky_after_hold_at = 0;
sticky_key->timer_cancelled = false;
sticky_key->timer_started = false;
sticky_key->modified_key_usage_page = 0;
@ -144,6 +147,8 @@ static int on_sticky_key_binding_pressed(struct zmk_behavior_binding *binding,
return ZMK_BEHAVIOR_OPAQUE;
}
sticky_key->no_sticky_after_hold_at = event.timestamp + cfg->no_sticky_after_hold_ms;
press_sticky_key_behavior(sticky_key, event.timestamp);
LOG_DBG("%d new sticky_key", event.position);
return ZMK_BEHAVIOR_OPAQUE;
@ -162,6 +167,11 @@ static int on_sticky_key_binding_released(struct zmk_behavior_binding *binding,
return release_sticky_key_behavior(sticky_key, event.timestamp);
}
if (sticky_key->no_sticky_after_hold_at < event.timestamp) {
LOG_DBG("Sticky key %d was held for longer than no-sticky-after-hold-ms.", event.position);
return release_sticky_key_behavior(sticky_key, event.timestamp);
}
// No other key was pressed. Start the timer.
sticky_key->timer_started = true;
sticky_key->release_at = event.timestamp + sticky_key->config->release_after_ms;
@ -290,6 +300,8 @@ static struct behavior_sticky_key_data behavior_sticky_key_data;
static struct behavior_sticky_key_config behavior_sticky_key_config_##n = { \
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, DT_DRV_INST(n)), \
.release_after_ms = DT_INST_PROP(n, release_after_ms), \
.no_sticky_after_hold_ms = \
DT_INST_PROP_OR(n, no_sticky_after_hold_ms, DT_INST_PROP(n, release_after_ms)), \
.ignore_modifiers = DT_INST_PROP(n, ignore_modifiers), \
.quick_release = DT_INST_PROP(n, quick_release), \
}; \

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -0,0 +1,10 @@
pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x1C implicit_mods 0x00 explicit_mods 0x00

View file

@ -0,0 +1,49 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>
#include "../behavior_keymap.dtsi"
/* This test ensures that sticky keys remain active while being pressed. */
/ {
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&sk E &sl 1
&kp A &kp B>;
};
lower_layer {
bindings = <
&sk LEFT_CONTROL &kp X
&kp Y &kp Z>;
};
};
};
&kscan {
events = <
// Hold &sk E past no-sticky-after-hold-ms
ZMK_MOCK_PRESS(0,0,1200)
// Tap &kp A twice
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
// &sk E should remain active before releasing
ZMK_MOCK_RELEASE(0,0,10)
// Hold &sl 1 past no-sticky-after-hold-ms
ZMK_MOCK_PRESS(0,1,1200)
// Tap &kp Y twice
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
// &sl 1 should remain active before releasing
ZMK_MOCK_RELEASE(0,1,10)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -0,0 +1,6 @@
pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00

View file

@ -0,0 +1,57 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>
#include "../behavior_keymap.dtsi"
/* This test ensures that sticky keys are deactivated immediately upon
* release after being held for longer than `no-sticky-after-hold-ms`.
*/
&sk {
no-sticky-after-hold-ms = <200>;
};
&sl {
no-sticky-after-hold-ms = <200>;
};
/ {
keymap {
compatible = "zmk,keymap";
label ="Default keymap";
default_layer {
bindings = <
&sk E &sl 1
&kp A &kp B>;
};
lower_layer {
bindings = <
&sk LEFT_CONTROL &kp X
&kp Y &kp Z>;
};
};
};
&kscan {
events = <
// Hold &sk E past no-sticky-after-hold-ms
ZMK_MOCK_PRESS(0,0,400)
// Release &sk E
ZMK_MOCK_RELEASE(0,0,10)
// &sk E should now be deactivated
// Tap &kp A right after
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
// Hold &sl 1 past no-sticky-after-hold-ms
ZMK_MOCK_PRESS(0,1,400)
// Release &sl 1
ZMK_MOCK_RELEASE(0,1,10)
// &sl 1 should now be deactivated
// Tap &kp A right after
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
>;
};

View file

@ -30,6 +30,10 @@ You can use any keycode that works for `&kp` as parameter to `&sk`:
By default, sticky keys stay pressed for a second if you don't press any other key. You can configure this with the `release-after-ms` setting.
#### `no-sticky-after-hold-ms`
Sticky keys that are pressed for longer than `no-sticky-after-hold-ms` will function like regular keys.
#### `quick-release`
Some typists may find that using a sticky shift key interspersed with rapid typing results in two or more capitalized letters instead of one. This happens as the sticky key is active until the next key is released, under which other keys may be pressed and will receive the modifier. You can enable the `quick-release` setting to instead deactivate the sticky key on the next key being pressed, as opposed to released.