implements oneshot/sticky keys.

This commit is contained in:
xs5871 2022-02-13 22:30:31 +00:00 committed by Kyle Brown
parent eb3a7bbf1e
commit ee4cce32cb
5 changed files with 142 additions and 14 deletions

View File

@ -11,6 +11,7 @@ modules are
put on your keyboard put on your keyboard
- [ModTap](modtap.md): Adds support for augmented modifier keys to act as one key - [ModTap](modtap.md): Adds support for augmented modifier keys to act as one key
when tapped, and modifier when held. when tapped, and modifier when held.
- [OneShot](oneshot.md): Adds support for oneshot/sticky keys.
- [Power](power.md): Power saving features. This is mostly useful when on battery power. - [Power](power.md): Power saving features. This is mostly useful when on battery power.
- [Split](split_keyboards.md): Keyboards split in two. Seems ergonomic! - [Split](split_keyboards.md): Keyboards split in two. Seems ergonomic!
- [TapDance](tapdance.md): Different key actions depending on how often it is pressed. - [TapDance](tapdance.md): Different key actions depending on how often it is pressed.

32
docs/oneshot.md Normal file
View File

@ -0,0 +1,32 @@
# OneShot Keycodes
OneShot keys or sticky keys enable you to have keys that keep staying 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 hold.
## Enable OneShot Keys
```python
from kmk.modules.oneshot import OneShot
oneshot = OneShot()
# optional: set a custom tap timeout in ms (default: 1000ms)
# oneshot.tap_time = 1500
keyboard.modules.append(modtap)
```
## Keycodes
|Keycode | Aliases |Description |
|`KC.OS(KC.ANY)` | `KC.ONESHOT` |make a sticky version of `KC.ANY` |
`KC.ONESHOT` accepts any valid key code as argument, including modifiers and KMK
internal keys like momentary layer shifts.
## Custom OneShot Behavior
The full OneShot signature is as follows:
```python
KC.OS(
KC.TAP, # the sticky keycode
tap_time=None # length of the tap timeout in milliseconds
)
```

View File

@ -4,9 +4,10 @@ from kmk.modules import Module
class ActivationType: class ActivationType:
NOT_ACTIVATED = const(0) PRESSED = const(0)
HOLD_TIMEOUT = const(1) RELEASED = const(1)
INTERRUPTED = const(2) HOLD_TIMEOUT = const(2)
INTERRUPTED = const(3)
class HoldTapKeyState: class HoldTapKeyState:
@ -14,7 +15,7 @@ class HoldTapKeyState:
self.timeout_key = timeout_key self.timeout_key = timeout_key
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.activated = ActivationType.NOT_ACTIVATED self.activated = ActivationType.PRESSED
class HoldTap(Module): class HoldTap(Module):
@ -39,7 +40,7 @@ class HoldTap(Module):
for key, state in self.key_states.items(): for key, state in self.key_states.items():
if key == current_key: if key == current_key:
continue continue
if state.activated != ActivationType.NOT_ACTIVATED: if state.activated != ActivationType.PRESSED:
continue continue
# holdtap is interrupted by another key event. # holdtap is interrupted by another key event.
@ -102,7 +103,7 @@ class HoldTap(Module):
elif state.activated == ActivationType.INTERRUPTED: elif state.activated == ActivationType.INTERRUPTED:
# release tap # release tap
self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs) self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs)
else: elif state.activated == ActivationType.PRESSED:
# press and release tap because key released within tap time # press and release tap because key released within tap time
self.ht_activate_tap(key, keyboard, *args, **kwargs) self.ht_activate_tap(key, keyboard, *args, **kwargs)
keyboard.set_timeout( keyboard.set_timeout(
@ -114,15 +115,23 @@ class HoldTap(Module):
return keyboard return keyboard
def on_tap_time_expired(self, key, keyboard, *args, **kwargs): def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
'''When tap time expires activate mod if key is still being pressed.''' '''When tap time expires activate hold if key is still being pressed.
if ( Remove key if ActivationType is RELEASED.'''
key in self.key_states try:
and self.key_states[key].activated == ActivationType.NOT_ACTIVATED state = self.key_states[key]
): except KeyError:
if keyboard.debug_enabled:
print(f'HoldTap.on_tap_time_expired: no such key {key}')
return
if self.key_states[key].activated == ActivationType.PRESSED:
# press hold because timer expired after tap time # press hold because timer expired after tap time
self.key_states[key].activated = ActivationType.HOLD_TIMEOUT self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
self.ht_activate_hold(key, keyboard, *args, **kwargs) self.ht_activate_hold(key, keyboard, *args, **kwargs)
self.send_key_buffer(keyboard) self.send_key_buffer(keyboard)
elif state.activated == ActivationType.RELEASED:
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
del self.key_states[key]
def send_key_buffer(self, keyboard): def send_key_buffer(self, keyboard):
for (int_coord, key) in self.key_buffer: for (int_coord, key) in self.key_buffer:

66
kmk/modules/oneshot.py Normal file
View File

@ -0,0 +1,66 @@
from kmk.keys import make_argumented_key
from kmk.modules.holdtap import ActivationType, HoldTap
from kmk.types import HoldTapKeyMeta
def oneshot_validator(kc, tap_time=None):
return HoldTapKeyMeta(kc=kc, prefer_hold=False, tap_time=tap_time)
class OneShot(HoldTap):
tap_time = 1000
def __init__(self):
super().__init__()
make_argumented_key(
validator=oneshot_validator,
names=('OS', 'ONESHOT'),
on_press=self.osk_pressed,
on_release=self.osk_released,
)
def process_key(self, keyboard, current_key, is_pressed, int_coord):
'''Release os key after interrupting keyup.'''
for key, state in self.key_states.items():
if key == current_key:
continue
if state.activated == ActivationType.PRESSED and is_pressed:
state.activated = ActivationType.HOLD_TIMEOUT
elif state.activated == ActivationType.RELEASED and is_pressed:
state.activated = ActivationType.INTERRUPTED
elif state.activated == ActivationType.INTERRUPTED and not is_pressed:
self.ht_released(key, keyboard)
return current_key
def osk_pressed(self, key, keyboard, *args, **kwargs):
'''Register HoldTap mechanism and activate os key.'''
self.ht_pressed(key, keyboard, *args, **kwargs)
self.ht_activate_tap(key, keyboard, *args, **kwargs)
return keyboard
def osk_released(self, key, keyboard, *args, **kwargs):
'''On keyup, mark os key as released or handle HoldTap.'''
try:
state = self.key_states[key]
except KeyError:
if keyboard.debug_enabled:
print(f'OneShot.osk_released: no such key {key}')
return keyboard
if state.activated == ActivationType.PRESSED:
state.activated = ActivationType.RELEASED
else:
self.ht_released(key, keyboard, *args, **kwargs)
return keyboard
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
keyboard.process_key(key.meta.kc, True)
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
keyboard.process_key(key.meta.kc, False)
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
keyboard.process_key(key.meta.kc, False)

View File

@ -3,16 +3,17 @@ import unittest
from kmk.keys import KC from kmk.keys import KC
from kmk.modules.layers import Layers from kmk.modules.layers import Layers
from kmk.modules.modtap import ModTap from kmk.modules.modtap import ModTap
from kmk.modules.oneshot import OneShot
from tests.keyboard_test import KeyboardTest from tests.keyboard_test import KeyboardTest
class TestHoldTap(unittest.TestCase): class TestHoldTap(unittest.TestCase):
def test_basic_kmk_keyboard(self): def test_basic_kmk_keyboard(self):
keyboard = KeyboardTest( keyboard = KeyboardTest(
[Layers(), ModTap()], [Layers(), ModTap(), OneShot()],
[ [
[KC.MT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D], [KC.MT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D, KC.OS(KC.E)],
[KC.N1, KC.N2, KC.N3, KC.N4], [KC.N1, KC.N2, KC.N3, KC.N4, KC.N5],
], ],
debug_enabled=False, debug_enabled=False,
) )
@ -101,6 +102,25 @@ class TestHoldTap(unittest.TestCase):
# TODO test TT # TODO test TT
# OS
keyboard.test(
'OS timed out',
[(4, True), (4, False), 1050],
[{KC.E}, {}],
)
keyboard.test(
'OS interrupt within tap time',
[(4, True), (4, False), 100, (3, True), (3, False)],
[{KC.E}, {KC.D, KC.E}, {}],
)
keyboard.test(
'OS hold with multiple interrupt keys',
[(4, True), 100, (3, True), (3, False), (2, True), (2, False), (4, False)],
[{KC.E}, {KC.D, KC.E}, {KC.E}, {KC.C, KC.E}, {KC.E}, {}],
)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()