Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add permissive_hold option to holdtap #653

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/en/modtap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 27 additions & 5 deletions kmk/modules/holdtap.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ def __init__(
tap,
hold,
prefer_hold=True,
permissive_hold=False,
tap_interrupted=False,
tap_time=None,
repeat=HoldTapRepeat.NONE,
):
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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -249,15 +271,15 @@ 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)

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)
70 changes: 70 additions & 0 deletions tests/test_hold_tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()