diff --git a/docs/en/modtap.md b/docs/en/modtap.md index edf65a0d4..a2c52a712 100644 --- a/docs/en/modtap.md +++ b/docs/en/modtap.md @@ -31,11 +31,14 @@ keyboard.modules.append(modtap) ## Custom HoldTap Behavior The full ModTap signature is as follows: ```python -KC.MT(KC.TAP, KC.HOLD, prefer_hold=True, tap_interrupted=False, tap_time=None, repeat=HoldTapRepeat.NONE) +KC.MT(KC.TAP, KC.HOLD, prefer_hold=True, permissive_hold=False, tap_interrupted=False, tap_time=None, repeat=HoldTapRepeat.NONE) ``` * `prefer_hold`: decides which keycode the ModTap key resolves to when another key is pressed before the timeout finishes. When `True` the hold keycode is chosen, the tap keycode when `False`. +* `permissive_hold`: decides which keycode the ModTap key resolves to when + another key is pressed and released before the timeout finishes. When + `True` the hold keycode is chosen, the tap keycode when `False`. * `tap_interrupted`: decides if the timeout will interrupt at the first other key press/down, or after the first other key up/release. Set to `True` for interrupt on release. diff --git a/kmk/modules/holdtap.py b/kmk/modules/holdtap.py index 92e61204e..ea3a39f9e 100644 --- a/kmk/modules/holdtap.py +++ b/kmk/modules/holdtap.py @@ -36,6 +36,7 @@ def __init__( tap, hold, prefer_hold=True, + permissive_hold=False, tap_interrupted=False, tap_time=None, repeat=HoldTapRepeat.NONE, @@ -43,6 +44,7 @@ def __init__( self.tap = tap self.hold = hold self.prefer_hold = prefer_hold + self.permissive_hold = permissive_hold self.tap_interrupted = tap_interrupted self.tap_time = tap_time self.repeat = repeat @@ -84,8 +86,13 @@ def process_key(self, keyboard, key, is_pressed, int_coord): continue # holdtap is interrupted by another key event. - if (is_pressed and not key.meta.tap_interrupted) or ( - not is_pressed and key.meta.tap_interrupted and self.key_buffer + if ( + is_pressed + and not (key.meta.tap_interrupted or key.meta.permissive_hold) + ) or ( + not is_pressed + and (key.meta.tap_interrupted or key.meta.permissive_hold) + and self.key_buffer ): keyboard.cancel_timeout(state.timeout_key) @@ -98,12 +105,27 @@ def process_key(self, keyboard, key, is_pressed, int_coord): # if interrupt on release: store interrupting keys until one of them # is released. if ( - key.meta.tap_interrupted + (key.meta.tap_interrupted or key.meta.permissive_hold) and is_pressed and not isinstance(current_key.meta, HoldTapKeyMeta) ): append_buffer = True + # if tapping another modtap key during a permissive hold, treat it + # as an interruption on release + if ( + key.meta.permissive_hold + and not is_pressed + and isinstance(current_key.meta, HoldTapKeyMeta) + and current_key in self.key_states + and self.key_states[current_key].activated == ActivationType.PRESSED + ): + keyboard.cancel_timeout(state.timeout_key) + self.key_states[key].activated = ActivationType.INTERRUPTED + self.ht_activate_on_interrupt( + key, keyboard, *state.args, **state.kwargs + ) + # apply changes with 'side-effects' on key_states or the loop behaviour # outside the loop. if append_buffer: @@ -249,7 +271,7 @@ def ht_deactivate_tap(self, key, keyboard, *args, **kwargs): def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_activate_on_interrupt') - if key.meta.prefer_hold: + if key.meta.prefer_hold or key.meta.permissive_hold: self.ht_activate_hold(key, keyboard, *args, **kwargs) else: self.ht_activate_tap(key, keyboard, *args, **kwargs) @@ -257,7 +279,7 @@ def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs): def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_deactivate_on_interrupt') - if key.meta.prefer_hold: + if key.meta.prefer_hold or key.meta.permissive_hold: self.ht_deactivate_hold(key, keyboard, *args, **kwargs) else: self.ht_deactivate_tap(key, keyboard, *args, **kwargs) diff --git a/tests/test_hold_tap.py b/tests/test_hold_tap.py index da2e9b399..766177074 100644 --- a/tests/test_hold_tap.py +++ b/tests/test_hold_tap.py @@ -419,6 +419,76 @@ def test_oneshot(self): [{KC.E}, {KC.D, KC.E}, {KC.E}, {KC.C, KC.E}, {KC.E}, {}], ) + def test_holdtap_permissive_hold(self): + keyboard = KeyboardTest( + [ModTap()], + [ + [ + KC.MT( + KC.A, + KC.LSHIFT, + tap_time=50, + prefer_hold=False, + permissive_hold=True, + ), + KC.B, + KC.MT(KC.C, KC.LCTL, tap_time=50, prefer_hold=False), + ] + ], + debug_enabled=False, + ) + + t_within = 40 + t_after = 60 + + keyboard.test( + 'nested tap', + [ + (0, True), + t_within, + (1, True), + (1, False), + (0, False), + ], + [{KC.LSHIFT}, {KC.LSHIFT, KC.B}, {KC.LSHIFT}, {}], + ) + + keyboard.test( + 'standard hold tap', + [ + (0, True), + t_after, + (1, True), + (1, False), + (0, False), + ], + [{KC.LSHIFT}, {KC.LSHIFT, KC.B}, {KC.LSHIFT}, {}], + ) + + keyboard.test( + 'key released after mod', + [ + (0, True), + t_within, + (1, True), + (0, False), + (1, False), + ], + [{KC.A}, {KC.B, KC.A}, {KC.B}, {}], + ) + + keyboard.test( + 'nested modtap', + [ + (0, True), + t_within, + (2, True), + (2, False), + (0, False), + ], + [{KC.LSHIFT}, {KC.LSHIFT, KC.C}, {KC.LSHIFT}, {}], + ) + if __name__ == '__main__': unittest.main()