diff --git a/kmk/modules/combos.py b/kmk/modules/combos.py index 1ac1b7f..77a2e8b 100644 --- a/kmk/modules/combos.py +++ b/kmk/modules/combos.py @@ -4,22 +4,35 @@ from kmk.modules import Module class Combo: - timeout = 50 + fast_reset = False per_key_timeout = False - _timeout = None + timeout = 50 _remaining = [] + _timeout = None - def __init__(self, match, result, timeout=None, per_key_timeout=None): + def __init__( + self, + match, + result, + fast_reset=None, + per_key_timeout=None, + 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: + if fast_reset is not None: + self.fast_reset = fast_reset + if per_key_timeout is not None: self.per_key_timeout = per_key_timeout + if timeout is not None: + self.timeout = timeout + + def __repr__(self): + return f'{self.__class__.__name__}({[k.code for k in self.match]})' def matches(self, key): raise NotImplementedError @@ -38,8 +51,9 @@ class Chord(Combo): class Sequence(Combo): - timeout = 1000 + fast_reset = True per_key_timeout = True + timeout = 1000 def matches(self, key): try: @@ -147,13 +161,57 @@ class Combos(Module): if key in combo.match: # Deactivate combo if it matches current key. self.deactivate(keyboard, combo) - self.reset_combo(keyboard, combo) + + if combo.fast_reset: + self.reset_combo(keyboard, combo) + self._key_buffer = [] + else: + combo._remaining.insert(0, key) + self._matching.append(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: + # Non-active but matching combos can either activate on key release + # if they're the only match, or "un-match" the released key but stay + # matching if they're a repeatable combo. + for combo in self._matching.copy(): + if key not in combo.match: + continue + + # Combo matches, but first key released before timeout. + elif not combo._remaining: + keyboard.cancel_timeout(combo._timeout) + self._matching.remove(combo) + self.activate(keyboard, combo) + self._key_buffer = [] + keyboard._send_hid() + self.deactivate(keyboard, combo) + if combo.fast_reset: + self.reset_combo(keyboard, combo) + else: + combo._remaining.insert(0, key) + self._matching.append(combo) + + # Skip combos that allow tapping. + elif combo.fast_reset: + continue + + # This was the last key released of a repeatable combo. + elif len(combo._remaining) == len(combo.match) - 1: + self._matching.remove(combo) + self.reset_combo(keyboard, combo) + self.send_key_buffer(keyboard) + self._key_buffer = [] + + # Anything between first and last key released. + else: + combo._remaining.insert(0, key) + + # Don't propagate key-release events for keys that have been + # buffered. Append release events only if corresponding press is in + # buffer. pressed = self._key_buffer.count((int_coord, key, True)) released = self._key_buffer.count((int_coord, key, False)) if (pressed - released) > 0: @@ -170,13 +228,8 @@ class Combos(Module): if not combo._remaining: self.activate(keyboard, combo) - if any([not pressed for (int_coord, 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 = [] + self.reset(keyboard) else: if not self._matching: # This was the last pending combo: flush key buffer. @@ -216,4 +269,5 @@ class Combos(Module): def reset(self, keyboard): self._matching = [] for combo in self.combos: - self.reset_combo(keyboard, combo) + if combo not in self._active: + self.reset_combo(keyboard, combo)