from micropython import const from kmk.keys import KC, make_argumented_key from kmk.modules import Module from kmk.utils import Debug debug = Debug(__name__) class ActivationType: PRESSED = const(0) RELEASED = const(1) HOLD_TIMEOUT = const(2) INTERRUPTED = const(3) REPEAT = const(4) class HoldTapRepeat: NONE = const(0) TAP = const(1) HOLD = const(2) ALL = const(3) class HoldTapKeyState: def __init__(self, timeout_key, *args, **kwargs): self.timeout_key = timeout_key self.args = args self.kwargs = kwargs self.activated = ActivationType.PRESSED class HoldTapKeyMeta: def __init__( self, tap, hold, prefer_hold=True, tap_interrupted=False, tap_time=None, repeat=HoldTapRepeat.NONE, ): self.tap = tap self.hold = hold self.prefer_hold = prefer_hold self.tap_interrupted = tap_interrupted self.tap_time = tap_time self.repeat = repeat class HoldTap(Module): tap_time = 300 def __init__(self): self.key_buffer = [] self.key_states = {} if KC.get('HT') == KC.NO: make_argumented_key( validator=HoldTapKeyMeta, names=('HT',), on_press=self.ht_pressed, on_release=self.ht_released, ) def during_bootup(self, keyboard): return def before_matrix_scan(self, keyboard): return def after_matrix_scan(self, keyboard): return def process_key(self, keyboard, key, is_pressed, int_coord): '''Handle holdtap being interrupted by another key press/release.''' current_key = key send_buffer = False append_buffer = False for key, state in self.key_states.items(): if key == current_key: continue if state.activated != ActivationType.PRESSED: continue # holdtap isn't interruptable, resolves on ht_release or timeout. if not key.meta.tap_interrupted and not key.meta.prefer_hold: append_buffer = True 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 ): keyboard.cancel_timeout(state.timeout_key) self.key_states[key].activated = ActivationType.INTERRUPTED self.ht_activate_on_interrupt( key, keyboard, *state.args, **state.kwargs ) append_buffer = True send_buffer = True # if interrupt on release: store interrupting keys until one of them # is released. if key.meta.tap_interrupted and is_pressed: append_buffer = True # apply changes with 'side-effects' on key_states or the loop behaviour # outside the loop. if append_buffer: self.key_buffer.append((int_coord, current_key, is_pressed)) current_key = None if send_buffer: self.send_key_buffer(keyboard) return current_key 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 ht_pressed(self, key, keyboard, *args, **kwargs): '''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.''' if key in self.key_states: state = self.key_states[key] keyboard.cancel_timeout(self.key_states[key].timeout_key) if state.activated == ActivationType.RELEASED: state.activated = ActivationType.REPEAT self.ht_activate_tap(key, keyboard, *args, **kwargs) elif state.activated == ActivationType.HOLD_TIMEOUT: self.ht_activate_hold(key, keyboard, *args, **kwargs) elif state.activated == ActivationType.INTERRUPTED: self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs) return if key.meta.tap_time is None: tap_time = self.tap_time else: tap_time = key.meta.tap_time timeout_key = keyboard.set_timeout( tap_time, lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs), ) self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs) return keyboard def ht_released(self, key, keyboard, *args, **kwargs): '''On keyup, release mod or tap key.''' if key not in self.key_states: return keyboard state = self.key_states[key] keyboard.cancel_timeout(state.timeout_key) repeat = key.meta.repeat & HoldTapRepeat.TAP if state.activated == ActivationType.HOLD_TIMEOUT: # release hold self.ht_deactivate_hold(key, keyboard, *args, **kwargs) repeat = key.meta.repeat & HoldTapRepeat.HOLD elif state.activated == ActivationType.INTERRUPTED: # release tap self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs) if key.meta.prefer_hold: repeat = key.meta.repeat & HoldTapRepeat.HOLD elif state.activated == ActivationType.PRESSED: # press and release tap because key released within tap time self.ht_activate_tap(key, keyboard, *args, **kwargs) self.send_key_buffer(keyboard) self.ht_deactivate_tap(key, keyboard, *args, **kwargs) state.activated = ActivationType.RELEASED self.send_key_buffer(keyboard) elif state.activated == ActivationType.REPEAT: state.activated = ActivationType.RELEASED self.ht_deactivate_tap(key, keyboard, *args, **kwargs) # don't delete the key state right now in this case if repeat: if key.meta.tap_time is None: tap_time = self.tap_time else: tap_time = key.meta.tap_time state.timeout_key = keyboard.set_timeout( tap_time, lambda: self.key_states.pop(key) ) else: del self.key_states[key] return keyboard def on_tap_time_expired(self, key, keyboard, *args, **kwargs): '''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 debug.enabled: debug(f'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): if not self.key_buffer: return reprocess = False for (int_coord, key, is_pressed) in self.key_buffer: keyboard.resume_process_key(self, key, is_pressed, int_coord, reprocess) if isinstance(key.meta, HoldTapKeyMeta): reprocess = True self.key_buffer.clear() def ht_activate_hold(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_activate_hold') keyboard.resume_process_key(self, key.meta.hold, True) def ht_deactivate_hold(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_deactivate_hold') keyboard.resume_process_key(self, key.meta.hold, False) def ht_activate_tap(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_activate_tap') keyboard.resume_process_key(self, key.meta.tap, True) def ht_deactivate_tap(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_deactivate_tap') keyboard.resume_process_key(self, key.meta.tap, False) def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs): if debug.enabled: debug('ht_activate_on_interrupt') if key.meta.prefer_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: self.ht_deactivate_hold(key, keyboard, *args, **kwargs) else: self.ht_deactivate_tap(key, keyboard, *args, **kwargs)