implements oneshot/sticky keys.
This commit is contained in:
		@@ -11,6 +11,7 @@ modules are
 | 
			
		||||
put on your keyboard
 | 
			
		||||
- [ModTap](modtap.md): Adds support for augmented modifier keys to act as one key
 | 
			
		||||
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.
 | 
			
		||||
- [Split](split_keyboards.md): Keyboards split in two. Seems ergonomic!
 | 
			
		||||
- [TapDance](tapdance.md): Different key actions depending on how often it is pressed.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								docs/oneshot.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								docs/oneshot.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
    )
 | 
			
		||||
```
 | 
			
		||||
@@ -4,9 +4,10 @@ from kmk.modules import Module
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivationType:
 | 
			
		||||
    NOT_ACTIVATED = const(0)
 | 
			
		||||
    HOLD_TIMEOUT = const(1)
 | 
			
		||||
    INTERRUPTED = const(2)
 | 
			
		||||
    PRESSED = const(0)
 | 
			
		||||
    RELEASED = const(1)
 | 
			
		||||
    HOLD_TIMEOUT = const(2)
 | 
			
		||||
    INTERRUPTED = const(3)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HoldTapKeyState:
 | 
			
		||||
@@ -14,7 +15,7 @@ class HoldTapKeyState:
 | 
			
		||||
        self.timeout_key = timeout_key
 | 
			
		||||
        self.args = args
 | 
			
		||||
        self.kwargs = kwargs
 | 
			
		||||
        self.activated = ActivationType.NOT_ACTIVATED
 | 
			
		||||
        self.activated = ActivationType.PRESSED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HoldTap(Module):
 | 
			
		||||
@@ -39,7 +40,7 @@ class HoldTap(Module):
 | 
			
		||||
        for key, state in self.key_states.items():
 | 
			
		||||
            if key == current_key:
 | 
			
		||||
                continue
 | 
			
		||||
            if state.activated != ActivationType.NOT_ACTIVATED:
 | 
			
		||||
            if state.activated != ActivationType.PRESSED:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # holdtap is interrupted by another key event.
 | 
			
		||||
@@ -102,7 +103,7 @@ class HoldTap(Module):
 | 
			
		||||
            elif state.activated == ActivationType.INTERRUPTED:
 | 
			
		||||
                # release tap
 | 
			
		||||
                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
 | 
			
		||||
                self.ht_activate_tap(key, keyboard, *args, **kwargs)
 | 
			
		||||
                keyboard.set_timeout(
 | 
			
		||||
@@ -114,15 +115,23 @@ class HoldTap(Module):
 | 
			
		||||
        return keyboard
 | 
			
		||||
 | 
			
		||||
    def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
 | 
			
		||||
        '''When tap time expires activate mod if key is still being pressed.'''
 | 
			
		||||
        if (
 | 
			
		||||
            key in self.key_states
 | 
			
		||||
            and self.key_states[key].activated == ActivationType.NOT_ACTIVATED
 | 
			
		||||
        ):
 | 
			
		||||
        '''When tap time expires activate hold if key is still being pressed.
 | 
			
		||||
        Remove key if ActivationType is RELEASED.'''
 | 
			
		||||
        try:
 | 
			
		||||
            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
 | 
			
		||||
            self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
 | 
			
		||||
            self.ht_activate_hold(key, keyboard, *args, **kwargs)
 | 
			
		||||
            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):
 | 
			
		||||
        for (int_coord, key) in self.key_buffer:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								kmk/modules/oneshot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								kmk/modules/oneshot.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@@ -3,16 +3,17 @@ import unittest
 | 
			
		||||
from kmk.keys import KC
 | 
			
		||||
from kmk.modules.layers import Layers
 | 
			
		||||
from kmk.modules.modtap import ModTap
 | 
			
		||||
from kmk.modules.oneshot import OneShot
 | 
			
		||||
from tests.keyboard_test import KeyboardTest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestHoldTap(unittest.TestCase):
 | 
			
		||||
    def test_basic_kmk_keyboard(self):
 | 
			
		||||
        keyboard = KeyboardTest(
 | 
			
		||||
            [Layers(), ModTap()],
 | 
			
		||||
            [Layers(), ModTap(), OneShot()],
 | 
			
		||||
            [
 | 
			
		||||
                [KC.MT(KC.A, KC.LCTL), KC.LT(1, KC.B), KC.C, KC.D],
 | 
			
		||||
                [KC.N1, KC.N2, KC.N3, KC.N4],
 | 
			
		||||
                [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.N5],
 | 
			
		||||
            ],
 | 
			
		||||
            debug_enabled=False,
 | 
			
		||||
        )
 | 
			
		||||
@@ -101,6 +102,25 @@ class TestHoldTap(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        # 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__':
 | 
			
		||||
    unittest.main()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user