diff --git a/kmk/common/consts.py b/kmk/common/consts.py index 064a9b0..426d488 100644 --- a/kmk/common/consts.py +++ b/kmk/common/consts.py @@ -143,3 +143,10 @@ class UnicodeModes: LINUX = IBUS = 1 MACOS = OSX = RALT = 2 WINC = 3 + + +class LeaderMode: + Default = 0 + Default_Active = 1 + Enter = 2 + Enter_Active = 3 diff --git a/kmk/common/event_defs.py b/kmk/common/event_defs.py index 51ba8eb..96e94bf 100644 --- a/kmk/common/event_defs.py +++ b/kmk/common/event_defs.py @@ -25,7 +25,6 @@ InitFirmware = namedtuple('InitFirmware', ( 'row_pins', 'col_pins', 'diode_orientation', - 'unicode_mode', )) KeyUpDown = namedtuple('KeyUpDown', ('type', 'row', 'col')) @@ -34,14 +33,13 @@ NewMatrix = namedtuple('NewMatrix', ('type', 'matrix')) BareEvent = namedtuple('BareEvent', ('type',)) -def init_firmware(keymap, row_pins, col_pins, diode_orientation, unicode_mode): +def init_firmware(keymap, row_pins, col_pins, diode_orientation): return InitFirmware( type=INIT_FIRMWARE_EVENT, keymap=keymap, row_pins=row_pins, col_pins=col_pins, diode_orientation=diode_orientation, - unicode_mode=unicode_mode, ) diff --git a/kmk/common/internal_state.py b/kmk/common/internal_state.py index 861cb13..9dc3014 100644 --- a/kmk/common/internal_state.py +++ b/kmk/common/internal_state.py @@ -2,7 +2,7 @@ import logging import sys from kmk.common import kmktime -from kmk.common.consts import DiodeOrientation, UnicodeModes +from kmk.common.consts import DiodeOrientation, LeaderMode, UnicodeModes from kmk.common.event_defs import (HID_REPORT_EVENT, INIT_FIRMWARE_EVENT, KEY_DOWN_EVENT, KEY_UP_EVENT, KEYCODE_DOWN_EVENT, KEYCODE_UP_EVENT, @@ -69,28 +69,31 @@ class InternalState: keys_pressed = set() pending_keys = set() macro_pending = None + leader_pending = None + leader_last_len = 0 hid_pending = False - unicode_mode = UnicodeModes.NOOP - tap_time = 300 keymap = [] row_pins = [] col_pins = [] matrix = [] diode_orientation = DiodeOrientation.COLUMNS + leader_mode_history = [] active_layers = [0] start_time = { 'lt': None, 'tg': None, 'tt': None, - } - tick_time = { - 'lt': None, - 'tg': None, - 'tt': None, + 'lm': None, + 'leader': None, } _oldstates = [] def __init__(self, preserve_intermediate_states=False): + import kmk_keyboard_user + self.unicode_mode = getattr(kmk_keyboard_user, 'unicode_mode', UnicodeModes.NOOP) + self.tap_time = getattr(kmk_keyboard_user, 'tap_time', 300) + self.leader_mode = getattr(kmk_keyboard_user, 'leader_mode', LeaderMode.Enter) + self.LEADER_DICTIONARY = getattr(kmk_keyboard_user, 'LEADER_DICTIONARY', {}) self.preserve_intermediate_states = preserve_intermediate_states def __enter__(self): @@ -105,8 +108,8 @@ class InternalState: 'active_layers': self.active_layers, 'unicode_mode': self.unicode_mode, 'tap_time': self.tap_time, + 'leader_mode_history': self.leader_mode_history, 'start_time': self.start_time, - 'tick_time': self.tick_time, } if verbose: @@ -196,18 +199,19 @@ def kmk_reducer(state=None, action=None, logger=None): state.matrix = action.matrix state.keys_pressed |= pressed state.keys_pressed -= released - state.hid_pending = True + if state.leader_mode % 2 == 1: + state.hid_pending = False + else: + state.hid_pending = True return state if action.type == KEYCODE_UP_EVENT: state.keys_pressed.discard(action.keycode) - state.hid_pending = True return state if action.type == KEYCODE_DOWN_EVENT: state.keys_pressed.add(action.keycode) - state.hid_pending = True return state if action.type == INIT_FIRMWARE_EVENT: @@ -216,7 +220,6 @@ def kmk_reducer(state=None, action=None, logger=None): row_pins=action.row_pins, col_pins=action.col_pins, diode_orientation=action.diode_orientation, - unicode_mode=action.unicode_mode, ) # HID events are non-mutating, used exclusively for listeners to know @@ -224,7 +227,6 @@ def kmk_reducer(state=None, action=None, logger=None): # into KEY_UP_EVENT and KEY_DOWN_EVENT, but for now it's nice to separate # this out for debugging's sake. if action.type == HID_REPORT_EVENT: - state.hid_pending = False return state if action.type == MACRO_COMPLETE_EVENT: @@ -268,6 +270,8 @@ def process_internal_key_event(state, action_type, changed_key, logger=None): return unicode_mode(state, action_type, changed_key, logger=logger) elif changed_key.code == RawKeycodes.KC_MACRO: return macro(state, action_type, changed_key, logger=logger) + elif changed_key.code == Keycodes.KMK.KC_LEAD.code: + return leader(state) else: return state @@ -410,3 +414,12 @@ def macro(state, action_type, changed_key, logger): return state return state + + +def leader(state): + if state.leader_mode % 2 == 0: + state.keys_pressed.discard(Keycodes.KMK.KC_LEAD) + # All leader modes are one number higher when activating + state.leader_mode += 1 + + return state diff --git a/kmk/common/leader_mode.py b/kmk/common/leader_mode.py new file mode 100644 index 0000000..d5fab75 --- /dev/null +++ b/kmk/common/leader_mode.py @@ -0,0 +1,86 @@ +import logging + +from kmk.common.keycodes import Keycodes + + +class LeaderHelper: + """ + Acts as a hid to absorb keypress, and perform macros when a timer + or enter key is pressed depending on the mode set. + """ + def __init__(self, store, log_level=logging.NOTSET): + self.logger = logging.getLogger(__name__) + self.logger.setLevel(log_level) + + self.store = store + self.store.subscribe( + lambda state, action: self._subscription(state, action), + ) + + def _subscription(self, state, action): + """ + Subscribes to the state machine, and dispatches actions based + based on incoming keypresses, or when a timer runs out depending + on the mode. + :param state: + :param action: + :return state: + """ + if state.leader_mode % 2 == 1: + keys_pressed = state.keys_pressed + + if state.leader_last_len and state.leader_mode_history: + history_set = set(state.leader_mode_history) + + keys_pressed = keys_pressed - history_set + + state.leader_last_len = len(state.keys_pressed) + + for key in keys_pressed: + if key == Keycodes.Common.KC_ENT: + # Process the action and remove the extra KC.ENT that was added to get here + state = process(state) + return clean_exit(state) + elif key == Keycodes.Common.KC_ESC: + # Clean state and turn leader mode off. + return clean_exit(state) + elif key == Keycodes.KMK.KC_LEAD: + return state + else: + # Add key if not needing to escape + # This needs replaced later with a proper debounce + state.leader_mode_history.append(key) + return state + + return state + + +def clean_exit(state): + """ + Cleans up the state and hands the HID control back. + :param state: + :return state: + """ + state.leader_mode_history = [] + state.leader_mode -= 1 + state.leader_last_len = 0 + state.keys_pressed.clear() + return state + + +def process(state): + """ + Checks if there are iny matching sequences of keys, and + performs the macro specified by the user. + :param state: + :param leader_dictionary: + :return state: + """ + lmh = tuple(state.leader_mode_history) + + if lmh in state.LEADER_DICTIONARY: + state.macro_pending = state.LEADER_DICTIONARY[lmh].keydown + + state.keys_pressed.clear() + + return state diff --git a/kmk/entrypoints/handwire/pyboard.py b/kmk/entrypoints/handwire/pyboard.py index 72b467f..97816c0 100644 --- a/kmk/entrypoints/handwire/pyboard.py +++ b/kmk/entrypoints/handwire/pyboard.py @@ -1,19 +1,28 @@ import sys -from logging import DEBUG -from kmk.common.consts import UnicodeModes +import gc + from kmk.firmware import Firmware from kmk.micropython.matrix import MatrixScanner from kmk.micropython.pyb_hid import HIDHelper def main(): - from kmk_keyboard_user import cols, diode_orientation, keymap, rows + import kmk_keyboard_user + cols = getattr(kmk_keyboard_user, 'cols') + diode_orientation = getattr(kmk_keyboard_user, 'diode_orientation') + keymap = getattr(kmk_keyboard_user, 'keymap') + rows = getattr(kmk_keyboard_user, 'rows') - try: - from kmk_keyboard_user import unicode_mode - except Exception: - unicode_mode = UnicodeModes.NOOP + DEBUG_ENABLE = getattr(kmk_keyboard_user, 'DEBUG_ENABLE', False) + + if DEBUG_ENABLE: + from logging import DEBUG + else: + from logging import ERROR as DEBUG + + # This will run out of ram at this point unless you manually GC + gc.collect() try: firmware = Firmware( @@ -21,11 +30,12 @@ def main(): row_pins=rows, col_pins=cols, diode_orientation=diode_orientation, - unicode_mode=unicode_mode, hid=HIDHelper, log_level=DEBUG, matrix_scanner=MatrixScanner, ) + # This will run out of ram at this point unless you manually GC + gc.collect() firmware.go() except Exception as e: diff --git a/kmk/firmware.py b/kmk/firmware.py index b58b3cf..706b05c 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -2,14 +2,16 @@ import logging from kmk.common.event_defs import init_firmware from kmk.common.internal_state import Store, kmk_reducer +from kmk.common.leader_mode import LeaderHelper class Firmware: def __init__( self, keymap, row_pins, col_pins, - diode_orientation, unicode_mode=None, - hid=None, log_level=logging.NOTSET, - matrix_scanner=None, + diode_orientation, unicode_mode=None, + hid=None, + log_level=logging.NOTSET, + matrix_scanner=None, ): assert matrix_scanner is not None self.matrix_scanner = matrix_scanner @@ -32,12 +34,13 @@ class Firmware: "Board will run in debug mode", ) + self.leader_helper = LeaderHelper(store=self.store, log_level=log_level) + self.store.dispatch(init_firmware( keymap=keymap, row_pins=row_pins, col_pins=col_pins, diode_orientation=diode_orientation, - unicode_mode=unicode_mode, )) def _subscription(self, state, action): diff --git a/kmk/micropython/pyb_hid.py b/kmk/micropython/pyb_hid.py index 97edc60..4144e09 100644 --- a/kmk/micropython/pyb_hid.py +++ b/kmk/micropython/pyb_hid.py @@ -34,6 +34,6 @@ class HIDHelper(AbstractHidHelper): # It'd be real awesome if pyb.USB_HID.send/recv would support # uselect.poll or uselect.select to more safely determine when # it is safe to write to the host again... - delay(5) + delay(1) return self diff --git a/user_keymaps/kdb424/handwire_planck_pyboard.py b/user_keymaps/kdb424/handwire_planck_pyboard.py index 3671270..9975c45 100644 --- a/user_keymaps/kdb424/handwire_planck_pyboard.py +++ b/user_keymaps/kdb424/handwire_planck_pyboard.py @@ -1,4 +1,4 @@ -from kmk.common.consts import DiodeOrientation, UnicodeModes +from kmk.common.consts import DiodeOrientation, LeaderMode, UnicodeModes from kmk.common.keycodes import KC from kmk.common.macros.unicode import unicode_sequence from kmk.common.pins import Pin as P @@ -9,9 +9,14 @@ rows = (P.Y1, P.Y2, P.Y3, P.Y4) diode_orientation = DiodeOrientation.COLUMNS -unicode_mode = UnicodeModes.LINUX -tap_time = 180 +# ------------------User level config variables --------------------------------------- +unicode_mode = UnicodeModes.LINUX +tap_time = 150 +leader_timeout = 2000 +DEBUG_ENABLE = True + +# -------------------------------Macros ----------------------------------------------- FLIP = unicode_sequence([ "28", "30ce", @@ -26,13 +31,74 @@ FLIP = unicode_sequence([ "253b", ]) +# ---------------------- Leader Key Macros -------------------------------------------- + +LEADER_DICTIONARY = { + (KC.F, KC.L, KC.I, KC.P): + unicode_sequence([ + "28", + "30ce", + "ca0", + "75ca", + "ca0", + "29", + "30ce", + "5f61", + "253b", + "2501", + "253b", + ]), + (KC.C, KC.H, KC.E, KC.E, KC.R): + unicode_sequence([ + '002B', + 'FF61', + '003A', + '002E', + 'FF9F', + '30FD', + '0028', + '00B4', + '2200', + 'FF61', + '0029', + 'FF89', + 'FF9F', + '002E', + '003A', + 'FF61', + '002B', + 'FF9F', + 'FF9F', + '002B', + 'FF61', + '003A', + '002E', + 'FF9F', + '30FD', + '0028', + '002A', + '00B4', + '2200', + '0029', + 'FF89', + 'FF9F', + '002E', + '003A', + 'FF61', + '002B', + 'FF9F', + ]), +} + +# ---------------------- Keymap --------------------------------------------------------- + keymap = [ [ # Default [KC.GESC, KC.QUOTE, KC.COMMA, KC.DOT, KC.P, KC.Y, KC.F, KC.G, KC.C, KC.R, KC.L, KC.BKSP], [KC.TAB, KC.A, KC.O, KC.E, KC.U, KC.I, KC.D, KC.H, KC.T, KC.N, KC.S, KC.ENT], [KC.LSFT, KC.SCLN, KC.Q, KC.J, KC.K, KC.X, KC.B, KC.M, KC.W, KC.V, KC.Z, KC.SLSH], - [KC.LCTRL, KC.LGUI, KC.LALT, KC.NO, KC.MO(2), KC.LT(3, KC.SPC), KC.LT(3, KC.SPC), KC.MO(4), KC.LEFT, KC.DOWN, KC.UP, KC.RIGHT], + [KC.LCTRL, KC.LGUI, KC.LALT, KC.LEAD, KC.MO(2), KC.LT(3, KC.SPC), KC.LT(3, KC.SPC), KC.MO(4), KC.LEFT, KC.DOWN, KC.UP, KC.RIGHT], ], [ # Gaming