implement combo/chord/sequence module
This commit is contained in:
parent
a8e7f43e59
commit
5c33fd3a9f
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
## [Modules](modules.md)
|
## [Modules](modules.md)
|
||||||
|
|
||||||
|
- [Combos](combos.md): Adds chords and sequences
|
||||||
- [Layers](layers.md): Adds layer support (Fn key) to allow many more keys to be put on your keyboard
|
- [Layers](layers.md): Adds layer support (Fn key) to allow many more keys to be put on your keyboard
|
||||||
- [ModTap](modtap.md): Adds support for augmented modifier keys to act as one key when tapped, and modifier when held.
|
- [ModTap](modtap.md): Adds support for augmented modifier keys to act as one key when tapped, and modifier when held.
|
||||||
- [Mouse keys](mouse_keys.md): Adds mouse keycodes
|
- [Mouse keys](mouse_keys.md): Adds mouse keycodes
|
||||||
|
45
docs/combos.md
Normal file
45
docs/combos.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Combos
|
||||||
|
Combos allow you to assign special functionality to combinations of key presses.
|
||||||
|
The two default behaviors are:
|
||||||
|
* Chords: match keys in random order, all pressed within 50ms.
|
||||||
|
* Sequences: match keys in order, pressed within 1s of one another.
|
||||||
|
|
||||||
|
You can define combos to listen to any valid KMK key, even internal or
|
||||||
|
functional keys, like HoldTap. When using internal KMK keys, be aware that the
|
||||||
|
order of modules matters.
|
||||||
|
|
||||||
|
The result of a combo is another key being pressed/released; if the desired
|
||||||
|
action isn't covered by KMK keys: create your own with `make_key` and attach
|
||||||
|
corresponding handlers.
|
||||||
|
|
||||||
|
Combos may overlap, i.e. share match keys amongst each other.
|
||||||
|
|
||||||
|
The optional arguments `timeout` and `per_key_timeout` define the time window
|
||||||
|
within which the match has to happen and wether the timeout is renewed after
|
||||||
|
each key press, respectively. These can be customized for every combo
|
||||||
|
individually.
|
||||||
|
|
||||||
|
## Keycodes
|
||||||
|
|New Keycode |Description |
|
||||||
|
|------------|----------------------------------------------------|
|
||||||
|
|`KC.LEADER` | a dummy / convenience key for leader key sequences |
|
||||||
|
|
||||||
|
## Example Code
|
||||||
|
```python
|
||||||
|
from kmk.keys import KC, make_key
|
||||||
|
from kmk.modules.combos import Combos, Chord, Sequence
|
||||||
|
combos = Combos()
|
||||||
|
keyboard.modules.append(combos)
|
||||||
|
|
||||||
|
make_key(
|
||||||
|
names=('MYKEY',),
|
||||||
|
on_press=lambda: print('I pressed MYKEY'),
|
||||||
|
)
|
||||||
|
|
||||||
|
combos.combos = [
|
||||||
|
Chord((KC.A, KC.B), KC.LSFT)
|
||||||
|
Chord((KC.A, KC.B, KC.C), KC.LALT)
|
||||||
|
Sequence((KC.LEADER, KC.A, KC.B), KC.C)
|
||||||
|
Sequence((KC.E, KC.F) KC.MYKEY, timeout=500, per_key_timeout=False)
|
||||||
|
]
|
||||||
|
```
|
@ -7,6 +7,7 @@ sandbox, and can make massive changes to normal operation.
|
|||||||
These modules are provided in all builds and can be enabled. Currently offered
|
These modules are provided in all builds and can be enabled. Currently offered
|
||||||
modules are
|
modules are
|
||||||
|
|
||||||
|
- [Combos](combos.md): Adds chords and sequences
|
||||||
- [Layers](layers.md): Adds layer support (Fn key) to allow many more keys to be
|
- [Layers](layers.md): Adds layer support (Fn key) to allow many more keys to be
|
||||||
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
|
||||||
|
@ -235,7 +235,7 @@ class KMKKeyboard:
|
|||||||
for k, v in timeouts:
|
for k, v in timeouts:
|
||||||
if k <= current_time:
|
if k <= current_time:
|
||||||
v()
|
v()
|
||||||
del self._timeouts[k]
|
self.cancel_timeout(k)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
196
kmk/modules/combos.py
Normal file
196
kmk/modules/combos.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import kmk.handlers.stock as handlers
|
||||||
|
from kmk.keys import make_key
|
||||||
|
from kmk.modules import Module
|
||||||
|
|
||||||
|
|
||||||
|
class Combo:
|
||||||
|
timeout = 50
|
||||||
|
per_key_timeout = False
|
||||||
|
_timeout = None
|
||||||
|
_remaining = []
|
||||||
|
|
||||||
|
def __init__(self, match, result, timeout=None, per_key_timeout=None):
|
||||||
|
'''
|
||||||
|
match: tuple of keys (KC.A, KC.B)
|
||||||
|
result: key KC.C
|
||||||
|
'''
|
||||||
|
self.match = match
|
||||||
|
self.result = result
|
||||||
|
if timeout:
|
||||||
|
self.timeout = timeout
|
||||||
|
if per_key_timeout:
|
||||||
|
self.per_key_timeout = per_key_timeout
|
||||||
|
|
||||||
|
def matches(self, key):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._remaining = list(self.match)
|
||||||
|
|
||||||
|
|
||||||
|
class Chord(Combo):
|
||||||
|
def matches(self, key):
|
||||||
|
try:
|
||||||
|
self._remaining.remove(key)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Sequence(Combo):
|
||||||
|
timeout = 1000
|
||||||
|
per_key_timeout = True
|
||||||
|
|
||||||
|
def matches(self, key):
|
||||||
|
try:
|
||||||
|
return key == self._remaining.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Combos(Module):
|
||||||
|
def __init__(self, combos=[]):
|
||||||
|
self.combos = combos
|
||||||
|
self._active = []
|
||||||
|
self._matching = []
|
||||||
|
self._reset = set()
|
||||||
|
self._key_buffer = []
|
||||||
|
|
||||||
|
make_key(
|
||||||
|
names=('LEADER',),
|
||||||
|
on_press=handlers.passthrough,
|
||||||
|
on_release=handlers.passthrough,
|
||||||
|
)
|
||||||
|
|
||||||
|
def during_bootup(self, keyboard):
|
||||||
|
self.reset(keyboard)
|
||||||
|
|
||||||
|
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, key, is_pressed, int_coord):
|
||||||
|
if is_pressed:
|
||||||
|
return self.on_press(keyboard, key)
|
||||||
|
else:
|
||||||
|
return self.on_release(keyboard, key)
|
||||||
|
|
||||||
|
def on_press(self, keyboard, key):
|
||||||
|
# refill potential matches from timed-out matches
|
||||||
|
if not self._matching:
|
||||||
|
self._matching = list(self._reset)
|
||||||
|
self._reset = set()
|
||||||
|
|
||||||
|
# filter potential matches
|
||||||
|
for combo in self._matching.copy():
|
||||||
|
if combo.matches(key):
|
||||||
|
continue
|
||||||
|
self._matching.remove(combo)
|
||||||
|
if combo._timeout:
|
||||||
|
keyboard.cancel_timeout(combo._timeout)
|
||||||
|
combo._timeout = keyboard.set_timeout(
|
||||||
|
combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._matching:
|
||||||
|
# At least one combo matches current key: append key to buffer.
|
||||||
|
self._key_buffer.append((key, True))
|
||||||
|
key = None
|
||||||
|
|
||||||
|
# Start or reset individual combo timeouts.
|
||||||
|
for combo in self._matching:
|
||||||
|
if combo._timeout:
|
||||||
|
if combo.per_key_timeout:
|
||||||
|
keyboard.cancel_timeout(combo._timeout)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
combo._timeout = keyboard.set_timeout(
|
||||||
|
combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# There's no matching combo: send and reset key buffer
|
||||||
|
self.send_key_buffer(keyboard)
|
||||||
|
self._key_buffer = []
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def on_release(self, keyboard, key):
|
||||||
|
for combo in self._active:
|
||||||
|
if key in combo.match:
|
||||||
|
# Deactivate combo if it matches current key.
|
||||||
|
self.deactivate(keyboard, combo)
|
||||||
|
self.reset_combo(keyboard, combo)
|
||||||
|
key = combo.result
|
||||||
|
break
|
||||||
|
|
||||||
|
# Don't propagate key-release events for keys that have been buffered.
|
||||||
|
# Append release events only if corresponding press is in buffer.
|
||||||
|
else:
|
||||||
|
pressed = self._key_buffer.count((key, True))
|
||||||
|
released = self._key_buffer.count((key, False))
|
||||||
|
if (pressed - released) > 0:
|
||||||
|
self._key_buffer.append((key, False))
|
||||||
|
key = None
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def on_timeout(self, keyboard, combo):
|
||||||
|
# If combo reaches timeout and has no remaining keys, activate it;
|
||||||
|
# else, drop it from the match list.
|
||||||
|
combo._timeout = None
|
||||||
|
self._matching.remove(combo)
|
||||||
|
|
||||||
|
if not combo._remaining:
|
||||||
|
self.activate(keyboard, combo)
|
||||||
|
if any([not pressed for (key, pressed) in self._key_buffer]):
|
||||||
|
# At least one of the combo keys has already been released:
|
||||||
|
# "tap" the combo result.
|
||||||
|
keyboard._send_hid()
|
||||||
|
self.deactivate(keyboard, combo)
|
||||||
|
self.reset(keyboard)
|
||||||
|
self._key_buffer = []
|
||||||
|
else:
|
||||||
|
if not self._matching:
|
||||||
|
# This was the last pending combo: flush key buffer.
|
||||||
|
self.send_key_buffer(keyboard)
|
||||||
|
self._key_buffer = []
|
||||||
|
self.reset_combo(keyboard, combo)
|
||||||
|
|
||||||
|
def send_key_buffer(self, keyboard):
|
||||||
|
for (key, is_pressed) in self._key_buffer:
|
||||||
|
keyboard.process_key(key, is_pressed)
|
||||||
|
keyboard._send_hid()
|
||||||
|
|
||||||
|
def activate(self, keyboard, combo):
|
||||||
|
combo.result.on_press(keyboard)
|
||||||
|
self._active.append(combo)
|
||||||
|
|
||||||
|
def deactivate(self, keyboard, combo):
|
||||||
|
combo.result.on_release(keyboard)
|
||||||
|
self._active.remove(combo)
|
||||||
|
|
||||||
|
def reset_combo(self, keyboard, combo):
|
||||||
|
combo.reset()
|
||||||
|
if combo._timeout is not None:
|
||||||
|
keyboard.cancel_timeout(combo._timeout)
|
||||||
|
combo._timeout = None
|
||||||
|
self._reset.add(combo)
|
||||||
|
|
||||||
|
def reset(self, keyboard):
|
||||||
|
self._matching = []
|
||||||
|
for combo in self.combos:
|
||||||
|
self.reset_combo(keyboard, combo)
|
373
tests/test_combos.py
Normal file
373
tests/test_combos.py
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from kmk.keys import KC
|
||||||
|
from kmk.modules.combos import Chord, Combos, Sequence
|
||||||
|
from tests.keyboard_test import KeyboardTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestCombo(unittest.TestCase):
|
||||||
|
def test_basic_kmk_keyboard(self):
|
||||||
|
combos = Combos()
|
||||||
|
combos.combos = [
|
||||||
|
Chord((KC.A, KC.B, KC.C), KC.Y),
|
||||||
|
Chord((KC.A, KC.B), KC.X),
|
||||||
|
Chord((KC.C, KC.D), KC.Z, timeout=80),
|
||||||
|
Sequence((KC.N1, KC.N2, KC.N3), KC.Y, timeout=50),
|
||||||
|
Sequence((KC.N1, KC.N2), KC.X, timeout=50),
|
||||||
|
Sequence((KC.N3, KC.N4), KC.Z, timeout=100),
|
||||||
|
Sequence((KC.N1, KC.N1, KC.N1), KC.W, timeout=50),
|
||||||
|
Sequence((KC.LEADER, KC.N1), KC.V, timeout=50),
|
||||||
|
]
|
||||||
|
keyboard = KeyboardTest(
|
||||||
|
[combos],
|
||||||
|
[
|
||||||
|
[KC.A, KC.B, KC.C, KC.D, KC.E, KC.F],
|
||||||
|
[KC.N1, KC.N2, KC.N3, KC.N4, KC.N5, KC.LEADER],
|
||||||
|
],
|
||||||
|
debug_enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
t_within = 40
|
||||||
|
t_after = 60
|
||||||
|
|
||||||
|
# test combos
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo, within timeout',
|
||||||
|
[(0, True), t_within, (1, True), (0, False), (1, False), t_after],
|
||||||
|
[{KC.X}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 3 combo, within timout, shuffled',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(2, True),
|
||||||
|
(1, True),
|
||||||
|
t_within,
|
||||||
|
(1, False),
|
||||||
|
(0, False),
|
||||||
|
(2, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.Y}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo + overlap, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
t_after,
|
||||||
|
(2, True),
|
||||||
|
(2, False),
|
||||||
|
2 * t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {}, {KC.C}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo + overlap, interleaved, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(2, True),
|
||||||
|
2 * t_after,
|
||||||
|
(0, False),
|
||||||
|
(2, False),
|
||||||
|
(1, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.X, KC.C}, {KC.C}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo hold + other, interleaved, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(4, True),
|
||||||
|
(4, False),
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.X, KC.E}, {KC.X}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo hold + overlap, interleaved, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(2, True),
|
||||||
|
(2, False),
|
||||||
|
2 * t_after,
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.X, KC.C}, {KC.X}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: other + 2 combo, after timeout',
|
||||||
|
[
|
||||||
|
(4, True),
|
||||||
|
t_after,
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.E}, {KC.E, KC.X}, {KC.E}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo + other, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(4, True),
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.E, KC.X}, {KC.E}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo + 2 combo, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(2, True),
|
||||||
|
(3, True),
|
||||||
|
2 * t_after,
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
(2, False),
|
||||||
|
(3, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.X, KC.Z}, {KC.Z}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 combo hold + 2 combo, after timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(2, True),
|
||||||
|
(3, True),
|
||||||
|
2 * t_after,
|
||||||
|
(2, False),
|
||||||
|
(3, False),
|
||||||
|
t_after,
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.X}, {KC.X, KC.Z}, {KC.X}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: partial combor, after timeout',
|
||||||
|
[(0, True), (0, False), t_after],
|
||||||
|
[{KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: partial combo, repeated',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {}, {KC.A}, {}, {KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: partial combo, repeated',
|
||||||
|
[
|
||||||
|
t_after,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
(1, True),
|
||||||
|
(1, False),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {}, {KC.B}, {}, {KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: 3 combo after timout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(2, True),
|
||||||
|
t_after,
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(1, False),
|
||||||
|
(0, False),
|
||||||
|
(2, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {KC.A, KC.C}, {KC.A, KC.C, KC.B}, {KC.A, KC.C}, {KC.C}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: other + 2 combo within timeout',
|
||||||
|
[
|
||||||
|
(4, True),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
t_after,
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.E}, {KC.E, KC.A}, {KC.E, KC.A, KC.B}, {KC.E, KC.A}, {KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: 2 + other combo within timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
t_within,
|
||||||
|
(1, True),
|
||||||
|
(4, True),
|
||||||
|
t_after,
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
(0, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {KC.A, KC.B}, {KC.A, KC.B, KC.E}, {KC.A, KC.E}, {KC.A}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: 2 Combo after timeout',
|
||||||
|
[(0, True), (0, False), t_after, (1, True), (1, False), t_after],
|
||||||
|
[{KC.A}, {}, {KC.B}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: Combo + other, within timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(1, True),
|
||||||
|
(4, True),
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {KC.A, KC.B}, {KC.A, KC.B, KC.E}, {KC.B, KC.E}, {KC.E}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: Combo + other, within timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(4, True),
|
||||||
|
(1, True),
|
||||||
|
(0, False),
|
||||||
|
(1, False),
|
||||||
|
(4, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.A}, {KC.A, KC.E}, {KC.A, KC.B, KC.E}, {KC.B, KC.E}, {KC.E}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# test sequences
|
||||||
|
keyboard.keyboard.active_layers = [1]
|
||||||
|
keyboard.test(
|
||||||
|
'match: leader sequence, within timeout',
|
||||||
|
[(5, True), (5, False), t_within, (0, True), (0, False), t_after],
|
||||||
|
[{KC.V}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 sequence, within timeout',
|
||||||
|
[(0, True), (0, False), t_within, (1, True), (1, False), t_after],
|
||||||
|
[{KC.X}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 2 sequence, within long timeout',
|
||||||
|
[(2, True), (2, False), 2 * t_within, (3, True), (3, False), 2 * t_after],
|
||||||
|
[{KC.Z}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 3 sequence, within timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
(1, True),
|
||||||
|
(1, False),
|
||||||
|
t_within,
|
||||||
|
(2, True),
|
||||||
|
(2, False),
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.Y}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'match: 3 sequence, same key, within timeout',
|
||||||
|
[
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
(0, True),
|
||||||
|
(0, False),
|
||||||
|
t_within,
|
||||||
|
t_after,
|
||||||
|
],
|
||||||
|
[{KC.W}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: 2 sequence, after timeout',
|
||||||
|
[(0, True), (0, False), t_after, (1, True), (1, False), t_after],
|
||||||
|
[{KC.N1}, {}, {KC.N2}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.test(
|
||||||
|
'no match: 2 sequence, out of order',
|
||||||
|
[(1, True), (1, False), t_within, (0, True), (0, False), t_after],
|
||||||
|
[{KC.N2}, {}, {KC.N1}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user