-
Notifications
You must be signed in to change notification settings - Fork 489
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
686 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Sticky Keys | ||
|
||
Sticky keys enable you to have keys that stay pressed for a certain time or | ||
until another key is pressed and released. | ||
If the timeout expires or other keys are pressed, and the sticky key wasn't | ||
released, it is handled as a regular key being held. | ||
|
||
## Enable Sticky Keys | ||
|
||
```python | ||
from kmk.modules.sticky_keys import StickyKeys | ||
sticky_keys = StickyKeys() | ||
# optional: set a custom release timeout in ms (default: 1000ms) | ||
# sticky_keys = StickyKeys(release_after=5000) | ||
keyboard.modules.append(sticky_keys) | ||
``` | ||
|
||
## Keycodes | ||
|
||
|Keycode | Aliases |Description | | ||
|-----------------|--------------|----------------------------------| | ||
|`KC.SK(KC.ANY)` | `KC.STICKY` |make a sticky version of `KC.ANY` | | ||
|
||
`KC.STICKY` accepts any valid key code as argument, including modifiers and KMK | ||
internal keys like momentary layer shifts. | ||
|
||
## Custom Sticky Behavior | ||
|
||
The full sticky key signature is as follows: | ||
|
||
```python | ||
KC.SK( | ||
KC.ANY, # the key to made sticky | ||
defer_release=False # when to release the key | ||
) | ||
``` | ||
|
||
### `defer_release` | ||
|
||
If `False` (default): release sticky key after the first interrupting key | ||
releases. | ||
If `True`: stay sticky until all keys are released. Useful when combined with | ||
non-sticky modifiers, layer keys, etc... | ||
|
||
## Sticky Stacks | ||
|
||
Sticky keys can be stacked, i.e. tapping a sticky key within the release timeout | ||
of another will reset the timeout off all previously tapped sticky keys and | ||
"stack" their effects. | ||
In this example if you tap `SK_LCTL` and then `SK_LSFT` followed by `KC.TAB`, | ||
the output will be `ctrl+shift+tab`. | ||
|
||
```python | ||
SK_LCTL = KC.SK(KC.LCTL) | ||
SK_LSFT = KC.SK(KC.LSFT) | ||
|
||
keyboard.keymap = [[SK_LSFT, SK_LCTL, KC.TAB]] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
from micropython import const | ||
|
||
from kmk.keys import make_argumented_key | ||
from kmk.utils import Debug | ||
|
||
debug = Debug(__name__) | ||
|
||
|
||
_SK_IDLE = const(0) | ||
_SK_PRESSED = const(1) | ||
_SK_RELEASED = const(2) | ||
_SK_HOLD = const(3) | ||
_SK_STICKY = const(4) | ||
|
||
|
||
class StickyKeyMeta: | ||
def __init__(self, key, defer_release=False): | ||
self.key = key | ||
self.defer_release = defer_release | ||
self.timeout = None | ||
self.state = _SK_IDLE | ||
|
||
|
||
class StickyKeys: | ||
def __init__(self, release_after=1000): | ||
self.active_keys = [] | ||
self.release_after = release_after | ||
|
||
make_argumented_key( | ||
validator=StickyKeyMeta, | ||
names=('SK', 'STICKY'), | ||
on_press=self.on_press, | ||
on_release=self.on_release, | ||
) | ||
|
||
def during_bootup(self, keyboard): | ||
return | ||
|
||
def before_matrix_scan(self, keyboard): | ||
return | ||
|
||
def after_matrix_scan(self, keyboard): | ||
return | ||
|
||
def before_hid_send(self, keyboard): | ||
return | ||
|
||
def after_hid_send(self, keyboard): | ||
return | ||
|
||
def on_powersave_enable(self, keyboard): | ||
return | ||
|
||
def on_powersave_disable(self, keyboard): | ||
return | ||
|
||
def process_key(self, keyboard, current_key, is_pressed, int_coord): | ||
delay_current = False | ||
|
||
for key in self.active_keys.copy(): | ||
# Ignore keys that will resolve to and emit a different key | ||
# eventually, potentially triggering twice. | ||
# Handle interactions among sticky keys (stacking) in `on_press` | ||
# instead of `process_key` to avoid race conditions / causal | ||
# reordering when resetting timeouts. | ||
if ( | ||
isinstance(current_key.meta, StickyKeyMeta) | ||
or current_key.meta.__class__.__name__ == 'TapDanceKeyMeta' | ||
or current_key.meta.__class__.__name__ == 'HoldTapKeyMeta' | ||
): | ||
continue | ||
|
||
meta = key.meta | ||
|
||
if meta.state == _SK_PRESSED and is_pressed: | ||
meta.state = _SK_HOLD | ||
elif meta.state == _SK_RELEASED and is_pressed: | ||
meta.state = _SK_STICKY | ||
elif meta.state == _SK_STICKY: | ||
# Defer sticky release until last other key is released. | ||
if meta.defer_release: | ||
if not is_pressed and len(keyboard._coordkeys_pressed) <= 1: | ||
self.deactivate(keyboard, key) | ||
# Release sticky key; if it's a new key pressed: delay | ||
# propagation until after the sticky release. | ||
else: | ||
self.deactivate(keyboard, key) | ||
delay_current = is_pressed | ||
|
||
if delay_current: | ||
keyboard.resume_process_key(self, current_key, is_pressed, int_coord, False) | ||
else: | ||
return current_key | ||
|
||
def set_timeout(self, keyboard, key): | ||
key.meta.timeout = keyboard.set_timeout( | ||
self.release_after, | ||
lambda: self.on_release_after(keyboard, key), | ||
) | ||
|
||
def on_press(self, key, keyboard, *args, **kwargs): | ||
# Let sticky keys stack by renewing timeouts. | ||
for sk in self.active_keys: | ||
keyboard.cancel_timeout(sk.meta.timeout) | ||
|
||
# Reset on repeated taps. | ||
if key.meta.state != _SK_IDLE: | ||
key.meta.state = _SK_PRESSED | ||
else: | ||
self.activate(keyboard, key) | ||
|
||
for sk in self.active_keys: | ||
self.set_timeout(keyboard, sk) | ||
|
||
def on_release(self, key, keyboard, *args, **kwargs): | ||
# No interrupt or timeout happend, mark key as RELEASED, ready to get | ||
# STICKY. | ||
if key.meta.state == _SK_PRESSED: | ||
key.meta.state = _SK_RELEASED | ||
# Key in HOLD state is handled like a regular release. | ||
elif key.meta.state == _SK_HOLD: | ||
for sk in self.active_keys.copy(): | ||
keyboard.cancel_timeout(sk.meta.timeout) | ||
self.deactivate(keyboard, sk) | ||
|
||
def on_release_after(self, keyboard, key): | ||
# Key is still pressed but nothing else happend: set to HOLD. | ||
if key.meta.state == _SK_PRESSED: | ||
for sk in self.active_keys: | ||
key.meta.state = _SK_HOLD | ||
keyboard.cancel_timeout(sk.meta.timeout) | ||
# Key got released but nothing else happend: deactivate. | ||
elif key.meta.state == _SK_RELEASED: | ||
for sk in self.active_keys.copy(): | ||
self.deactivate(keyboard, sk) | ||
|
||
def activate(self, keyboard, key): | ||
if debug.enabled: | ||
debug('activate') | ||
key.meta.state = _SK_PRESSED | ||
self.active_keys.insert(0, key) | ||
keyboard.resume_process_key(self, key.meta.key, True) | ||
|
||
def deactivate(self, keyboard, key): | ||
if debug.enabled: | ||
debug('deactivate') | ||
key.meta.state = _SK_IDLE | ||
self.active_keys.remove(key) | ||
keyboard.resume_process_key(self, key.meta.key, False) |
Oops, something went wrong.