diff --git a/kmk/firmware.py b/kmk/firmware.py index c59f580..ac8f3b8 100644 --- a/kmk/firmware.py +++ b/kmk/firmware.py @@ -202,16 +202,6 @@ class Firmware: if old_timeouts_len != new_timeouts_len: state_changed = True - if self._state.macros_pending: - # Blindly assume macros are going to change state, which is almost - # always a safe assumption - state_changed = True - for macro in self._state.macros_pending: - for key in macro(self): - self._send_key(key) - - self._state.resolve_macro() - if self.debug_enabled and state_changed: print('New State: {}'.format(self._state._to_dict())) diff --git a/kmk/handlers/__init__.py b/kmk/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmk/handlers/layers.py b/kmk/handlers/layers.py new file mode 100644 index 0000000..0e448dd --- /dev/null +++ b/kmk/handlers/layers.py @@ -0,0 +1,108 @@ +from kmk.kmktime import ticks_diff, ticks_ms + + +def df_pressed(key, state, *args, **kwargs): + """Switches the default layer""" + state.active_layers[0] = key.meta.layer + state.reversed_active_layers = list(reversed(state.active_layers)) + return state + + +def mo_pressed(key, state, *args, **kwargs): + """Momentarily activates layer, switches off when you let go""" + state.active_layers.append(key.meta.layer) + state.reversed_active_layers = list(reversed(state.active_layers)) + return state + + +def mo_released(key, state, KC, *args, **kwargs): + state.active_layers = [ + layer for layer in state.active_layers + if layer != key.meta.layer + ] + state.reversed_active_layers = list(reversed(state.active_layers)) + return state + + +def lm_pressed(key, state, *args, **kwargs): + """As MO(layer) but with mod active""" + state.hid_pending = True + # Sets the timer start and acts like MO otherwise + state.start_time['lm'] = ticks_ms() + state.keys_pressed.add(key.meta.kc) + return mo_pressed(key, state, *args, **kwargs) + + +def lm_released(key, state, *args, **kwargs): + """As MO(layer) but with mod active""" + state.hid_pending = True + state.keys_pressed.discard(key.meta.kc) + state.start_time['lm'] = None + return mo_released(key, state, *args, **kwargs) + + +def lt_pressed(key, state, *args, **kwargs): + # Sets the timer start and acts like MO otherwise + state.start_time['lt'] = ticks_ms() + return mo_pressed(key, state, *args, **kwargs) + + +def lt_released(key, state, *args, **kwargs): + # On keyup, check timer, and press key if needed. + if state.start_time['lt'] and ( + ticks_diff(ticks_ms(), state.start_time['lt']) < state.config.tap_time + ): + state.hid_pending = True + state.tap_key(key.meta.kc) + + mo_released(key, state, *args, **kwargs) + state.start_time['lt'] = None + return state + + +def tg_pressed(key, state, *args, **kwargs): + """Toggles the layer (enables it if not active, and vise versa)""" + if key.meta.layer in state.active_layers: + state.active_layers = [ + layer for layer in state.active_layers + if layer != key.meta.layer + ] + else: + state.active_layers.append(key.meta.layer) + + state.reversed_active_layers = list(reversed(state.active_layers)) + + return state + + +def to_pressed(key, state, *args, **kwargs): + """Activates layer and deactivates all other layers""" + state.active_layers = [key.meta.kc] + state.reversed_active_layers = list(reversed(state.active_layers)) + + return state + + +def tt_pressed(key, state, *args, **kwargs): + """Momentarily activates layer if held, toggles it if tapped repeatedly""" + # TODO Make this work with tap dance to function more correctly, but technically works. + if state.start_time['tt'] is None: + # Sets the timer start and acts like MO otherwise + state.start_time['tt'] = ticks_ms() + return mo_pressed(key, state, *args, **kwargs) + elif ticks_diff(ticks_ms(), state.start_time['tt']) < state.config.tap_time: + state.start_time['tt'] = None + return tg_pressed(key, state, *args, **kwargs) + + +def tt_released(key, state, *args, **kwargs): + if ( + state.start_time['tt'] is None or + ticks_diff(ticks_ms(), state.start_time['tt']) >= state.config.tap_time + ): + # On first press, works like MO. On second press, does nothing unless let up within + # time window, then acts like TG. + state.start_time['tt'] = None + return mo_released(key, state, *args, **kwargs) + + return state diff --git a/kmk/handlers/sequences.py b/kmk/handlers/sequences.py new file mode 100644 index 0000000..c218a5f --- /dev/null +++ b/kmk/handlers/sequences.py @@ -0,0 +1,40 @@ +from kmk.keycodes import ALL_KEYS, KC, make_key +from kmk.types import KeySequenceMeta + + +def sequence_press_handler(key, state, KC, *args, **kwargs): + old_keys_pressed = state.keys_pressed + state.keys_pressed = set() + + for ikey in key.meta.seq: + if not getattr(ikey, 'no_press', None): + state.process_key(ikey, True) + state.config._send_hid() + if not getattr(ikey, 'no_release', None): + state.process_key(ikey, False) + state.config._send_hid() + + state.keys_pressed = old_keys_pressed + + return state + + +def simple_key_sequence(seq): + return make_key( + meta=KeySequenceMeta(seq), + on_press=sequence_press_handler, + ) + + +def send_string(message): + seq = [] + + for char in message: + kc = ALL_KEYS[char] + + if char.isupper(): + kc = KC.LSHIFT(kc) + + seq.append(kc) + + return simple_key_sequence(seq) diff --git a/kmk/handlers/stock.py b/kmk/handlers/stock.py new file mode 100644 index 0000000..e117dd5 --- /dev/null +++ b/kmk/handlers/stock.py @@ -0,0 +1,89 @@ +from kmk.kmktime import sleep_ms +from kmk.util import reset_bootloader, reset_keyboard + + +def passthrough(key, state, *args, **kwargs): + return state + + +def default_pressed(key, state, KC, coord_int=None, coord_raw=None): + if coord_int is not None: + state.coord_keys_pressed[coord_int] = key + + state.add_key(key) + + return state + + +def default_released(key, state, KC, coord_int=None, coord_raw=None): + state.remove_key(key) + + if coord_int is not None: + state.keys_pressed.discard(key.coord_keys_pressed.get(coord_int, None)) + state.coord_keys_pressed[coord_int] = None + + return state + + +def reset(*args, **kwargs): + reset_keyboard() + + +def bootloader(*args, **kwargs): + reset_bootloader() + + +def debug_pressed(key, state, KC, *args, **kwargs): + if state.config.debug_enabled: + print('Disabling debug mode, bye!') + else: + print('Enabling debug mode. Welcome to the jungle.') + + state.config.debug_enabled = not state.config.debug_enabled + + return state + + +def gesc_pressed(key, state, KC, *args, **kwargs): + GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI} + + if GESC_TRIGGERS.intersection(state.keys_pressed): + # if Shift is held, KC_GRAVE will become KC_TILDE on OS level + state.keys_pressed.add(KC.GRAVE) + return state + + # else return KC_ESC + state.keys_pressed.add(KC.ESCAPE) + state.hid_pending = True + + return state + + +def gesc_released(key, state, KC, *args, **kwargs): + state.keys_pressed.discard(KC.ESCAPE) + state.keys_pressed.discard(KC.GRAVE) + state.hid_pending = True + return state + + +def sleep_pressed(key, state, KC, *args, **kwargs): + sleep_ms(key.meta.ms) + return state + + +def uc_mode_pressed(key, state, *args, **kwargs): + state.config.unicode_mode = key.meta.mode + + return state + + +def leader_pressed(key, state, *args, **kwargs): + return state._begin_leader_mode() + + +def tap_dance_pressed(key, state, *args, **kwargs): + return state._process_tap_dance(key, True) + + +def tap_dance_released(key, state, *args, **kwargs): + return state._process_tap_dance(key, False) diff --git a/kmk/internal_state.py b/kmk/internal_state.py index 5d5d429..1cf40d6 100644 --- a/kmk/internal_state.py +++ b/kmk/internal_state.py @@ -1,19 +1,13 @@ from kmk.consts import LeaderMode -from kmk.keycodes import (FIRST_KMK_INTERNAL_KEYCODE, Keycodes, RawKeycodes, - TapDanceKeycode) -from kmk.kmktime import sleep_ms, ticks_diff, ticks_ms +from kmk.keycodes import KC +from kmk.kmktime import ticks_ms +from kmk.types import TapDanceKeyMeta from kmk.util import intify_coordinate -GESC_TRIGGERS = { - Keycodes.Modifiers.KC_LSHIFT, Keycodes.Modifiers.KC_RSHIFT, - Keycodes.Modifiers.KC_LGUI, Keycodes.Modifiers.KC_RGUI, -} - class InternalState: keys_pressed = set() coord_keys_pressed = {} - macros_pending = [] leader_pending = None leader_last_len = 0 hid_pending = False @@ -34,22 +28,6 @@ class InternalState: def __init__(self, config): self.config = config - self.internal_key_handlers = { - RawKeycodes.KC_DF: self._layer_df, - RawKeycodes.KC_MO: self._layer_mo, - RawKeycodes.KC_LM: self._layer_lm, - RawKeycodes.KC_LT: self._layer_lt, - RawKeycodes.KC_TG: self._layer_tg, - RawKeycodes.KC_TO: self._layer_to, - RawKeycodes.KC_TT: self._layer_tt, - Keycodes.KMK.KC_GESC.code: self._kc_gesc, - RawKeycodes.KC_UC_MODE: self._kc_uc_mode, - RawKeycodes.KC_MACRO: self._kc_macro, - Keycodes.KMK.KC_LEAD.code: self._kc_lead, - Keycodes.KMK.KC_NO.code: self._kc_no, - Keycodes.KMK.KC_DEBUG.code: self._kc_debug_mode, - RawKeycodes.KC_TAP_DANCE: self._kc_tap_dance, - } def __repr__(self): return 'InternalState({})'.format(self._to_dict()) @@ -74,7 +52,7 @@ class InternalState: for layer in self.reversed_active_layers: layer_key = self.config.keymap[layer][row][col] - if not layer_key or layer_key == Keycodes.KMK.KC_TRNS: + if not layer_key or layer_key == KC.TRNS: continue if self.config.debug_enabled: @@ -122,16 +100,16 @@ class InternalState: print('No key accessible for col, row: {}, {}'.format(row, col)) return self - if self.tapping and not isinstance(kc_changed, TapDanceKeycode): - self._process_tap_dance(kc_changed, is_pressed) + return self.process_key(kc_changed, is_pressed, int_coord, (row, col)) + + def process_key(self, key, is_pressed, coord_int=None, coord_raw=None): + if self.tapping and not isinstance(key.meta, TapDanceKeyMeta): + self._process_tap_dance(key, is_pressed) else: if is_pressed: - self.coord_keys_pressed[int_coord] = kc_changed - self.add_key(kc_changed) + key.on_press(self, coord_int, coord_raw) else: - self.remove_key(kc_changed) - self.keys_pressed.discard(self.coord_keys_pressed.get(int_coord, None)) - self.coord_keys_pressed[int_coord] = None + key.on_release(self, coord_int, coord_raw) if self.config.leader_mode % 2 == 1: self._process_leader_mode() @@ -140,27 +118,11 @@ class InternalState: def remove_key(self, keycode): self.keys_pressed.discard(keycode) - - if keycode.code >= FIRST_KMK_INTERNAL_KEYCODE: - self._process_internal_key_event(keycode, False) - else: - self.hid_pending = True - - return self + return self.process_key(keycode, False) def add_key(self, keycode): - # TODO Make this itself a macro keycode with a keyup handler - # rather than handling this inline here. Gross. - if keycode.code == Keycodes.KMK.KC_MACRO_SLEEP_MS: - sleep_ms(keycode.ms) - else: - self.keys_pressed.add(keycode) - - if keycode.code >= FIRST_KMK_INTERNAL_KEYCODE: - self._process_internal_key_event(keycode, True) - else: - self.hid_pending = True - return self + self.keys_pressed.add(keycode) + return self.process_key(keycode, True) def tap_key(self, keycode): self.add_key(keycode) @@ -175,13 +137,6 @@ class InternalState: self.hid_pending = False return self - def resolve_macro(self): - if self.config.debug_enabled: - print('Macro complete!') - - self.macros_pending.pop() - return self - def _process_internal_key_event(self, changed_key, is_pressed): # Since the key objects can be chained into new objects # with, for example, no_press set, always check against @@ -192,164 +147,9 @@ class InternalState: changed_key, is_pressed, ) - def _layer_df(self, changed_key, is_pressed): - """Switches the default layer""" - if is_pressed: - self.active_layers[0] = changed_key.layer - self.reversed_active_layers = list(reversed(self.active_layers)) - - return self - - def _layer_mo(self, changed_key, is_pressed): - """Momentarily activates layer, switches off when you let go""" - if is_pressed: - self.active_layers.append(changed_key.layer) - else: - self.active_layers = [ - layer for layer in self.active_layers - if layer != changed_key.layer - ] - - self.reversed_active_layers = list(reversed(self.active_layers)) - - return self - - def _layer_lm(self, changed_key, is_pressed): - """As MO(layer) but with mod active""" - self.hid_pending = True - - if is_pressed: - # Sets the timer start and acts like MO otherwise - self.start_time['lm'] = ticks_ms() - self.keys_pressed.add(changed_key.kc) - else: - self.keys_pressed.discard(changed_key.kc) - self.start_time['lm'] = None - - return self._layer_mo(changed_key, is_pressed) - - def _layer_lt(self, changed_key, is_pressed): - """Momentarily activates layer if held, sends kc if tapped""" - if is_pressed: - # Sets the timer start and acts like MO otherwise - self.start_time['lt'] = ticks_ms() - self._layer_mo(changed_key, is_pressed) - else: - # On keyup, check timer, and press key if needed. - if self.start_time['lt'] and ( - ticks_diff(ticks_ms(), self.start_time['lt']) < self.config.tap_time - ): - self.hid_pending = True - self.tap_key(changed_key.kc) - - self._layer_mo(changed_key, is_pressed) - self.start_time['lt'] = None - return self - - def _layer_tg(self, changed_key, is_pressed): - """Toggles the layer (enables it if not active, and vise versa)""" - if is_pressed: - if changed_key.layer in self.active_layers: - self.active_layers = [ - layer for layer in self.active_layers - if layer != changed_key.layer - ] - else: - self.active_layers.append(changed_key.layer) - - self.reversed_active_layers = list(reversed(self.active_layers)) - - return self - - def _layer_to(self, changed_key, is_pressed): - """Activates layer and deactivates all other layers""" - if is_pressed: - self.active_layers = [changed_key.layer] - self.reversed_active_layers = list(reversed(self.active_layers)) - - return self - - def _layer_tt(self, changed_key, is_pressed): - """Momentarily activates layer if held, toggles it if tapped repeatedly""" - # TODO Make this work with tap dance to function more correctly, but technically works. - if is_pressed: - if self.start_time['tt'] is None: - # Sets the timer start and acts like MO otherwise - self.start_time['tt'] = ticks_ms() - return self._layer_mo(changed_key, is_pressed) - elif ticks_diff(ticks_ms(), self.start_time['tt']) < self.config.tap_time: - self.start_time['tt'] = None - return self.tg(changed_key, is_pressed) - elif ( - self.start_time['tt'] is None or - ticks_diff(ticks_ms(), self.start_time['tt']) >= self.config.tap_time - ): - # On first press, works like MO. On second press, does nothing unless let up within - # time window, then acts like TG. - self.start_time['tt'] = None - return self._layer_mo(changed_key, is_pressed) - - return self - - def _kc_uc_mode(self, changed_key, is_pressed): - if is_pressed: - self.config.unicode_mode = changed_key.mode - - return self - - def _kc_macro(self, changed_key, is_pressed): - if is_pressed: - if changed_key.keyup: - self.macros_pending.append(changed_key.keyup) - else: - if changed_key.keydown: - self.macros_pending.append(changed_key.keydown) - - return self - - def _kc_lead(self, changed_key, is_pressed): - if is_pressed: - self._begin_leader_mode() - - return self - - def _kc_gesc(self, changed_key, is_pressed): - self.hid_pending = True - - if is_pressed: - if GESC_TRIGGERS.intersection(self.keys_pressed): - # if Shift is held, KC_GRAVE will become KC_TILDE on OS level - self.keys_pressed.add(Keycodes.Common.KC_GRAVE) - return self - - # else return KC_ESC - self.keys_pressed.add(Keycodes.Common.KC_ESCAPE) - return self - - self.keys_pressed.discard(Keycodes.Common.KC_ESCAPE) - self.keys_pressed.discard(Keycodes.Common.KC_GRAVE) - return self - - def _kc_no(self, changed_key, is_pressed): - return self - - def _kc_debug_mode(self, changed_key, is_pressed): - if is_pressed: - if self.config.debug_enabled: - print('Disabling debug mode, bye!') - else: - print('Enabling debug mode. Welcome to the jungle.') - - self.config.debug_enabled = not self.config.debug_enabled - - return self - - def _kc_tap_dance(self, changed_key, is_pressed): - return self._process_tap_dance(changed_key, is_pressed) - def _process_tap_dance(self, changed_key, is_pressed): if is_pressed: - if not isinstance(changed_key, TapDanceKeycode): + if not isinstance(changed_key.meta, TapDanceKeyMeta): # If we get here, changed_key is not a TapDanceKeycode and thus # the user kept typing elsewhere (presumably). End ALL of the # currently outstanding tap dance runs. @@ -408,7 +208,7 @@ class InternalState: def _begin_leader_mode(self): if self.config.leader_mode % 2 == 0: - self.keys_pressed.discard(Keycodes.KMK.KC_LEAD) + self.keys_pressed.discard(KC.LEAD) # All leader modes are one number higher when activating self.config.leader_mode += 1 @@ -421,7 +221,7 @@ class InternalState: lmh = tuple(self.leader_mode_history) if lmh in self.config.leader_dictionary: - self.macros_pending.append(self.config.leader_dictionary[lmh].keydown) + self.process_key(self.config.leader_dictionary[lmh], True) return self._exit_leader_mode() @@ -438,15 +238,15 @@ class InternalState: for key in keys_pressed: if ( self.config.leader_mode == LeaderMode.ENTER_ACTIVE and - key == Keycodes.Common.KC_ENT + key == KC.ENT ): self._handle_leader_sequence() break - elif key == Keycodes.Common.KC_ESC or key == Keycodes.KMK.KC_GESC: + elif key == KC.ESC or key == KC.GESC: # Clean self and turn leader mode off. self._exit_leader_mode() break - elif key == Keycodes.KMK.KC_LEAD: + elif key == KC.LEAD: break else: # Add key if not needing to escape diff --git a/kmk/keycodes.py b/kmk/keycodes.py index ef0d6ea..2d0d528 100644 --- a/kmk/keycodes.py +++ b/kmk/keycodes.py @@ -1,110 +1,55 @@ -try: - from collections import namedtuple -except ImportError: - # This is handled by micropython-lib/collections, but on local runs of - # MicroPython, it doesn't exist - from ucollections import namedtuple - +import kmk.handlers.layers as layers +import kmk.handlers.stock as handlers from kmk.consts import UnicodeMode -from kmk.types import AttrDict +from kmk.types import (AttrDict, KeySeqSleepMeta, LayerKeyMeta, + TapDanceKeyMeta, UnicodeModeKeyMeta) FIRST_KMK_INTERNAL_KEYCODE = 1000 +NEXT_AVAILABLE_KEYCODE = 1000 -kc_lookup_cache = {} +KEYCODE_SIMPLE = 0 +KEYCODE_MODIFIER = 1 +KEYCODE_CONSUMER = 2 - -def lookup_kc_with_cache(char): - found_code = kc_lookup_cache.get( - char, - getattr(Common, 'KC_{}'.format(char.upper())), - ) - - kc_lookup_cache[char] = found_code - kc_lookup_cache[char.upper()] = found_code - kc_lookup_cache[char.lower()] = found_code - - return found_code - - -def generate_codepoint_keysym_seq(codepoint, expected_length=4): - # To make MacOS and Windows happy, always try to send - # sequences that are of length 4 at a minimum - # On Linux systems, we can happily send longer strings. - # They will almost certainly break on MacOS and Windows, - # but this is a documentation problem more than anything. - # Not sure how to send emojis on Mac/Windows like that, - # though, since (for example) the Canadian flag is assembled - # from two five-character codepoints, 1f1e8 and 1f1e6 - # - # As a bonus, this function can be pretty useful for - # leader dictionary keys as strings. - seq = [Common.KC_0 for _ in range(max(len(codepoint), expected_length))] - - for idx, codepoint_fragment in enumerate(reversed(codepoint)): - seq[-(idx + 1)] = lookup_kc_with_cache(codepoint_fragment) - - return seq +# Global state, will be filled in througout this file, and +# anywhere the user creates custom keys +ALL_KEYS = {} +KC = AttrDict(ALL_KEYS) def generate_leader_dictionary_seq(string): + # FIXME move to kmk.macros.unicode or somewhere else more fitting + # left here for backwards compat with various keymaps that + # import this + # + # I have absolutely no idea why it was in this file to begin with, + # probably something related to import order at one point. + from kmk.macros.unicode import generate_codepoint_keysym_seq + return tuple(generate_codepoint_keysym_seq(string, 1)) -class RawKeycodes: - ''' - These are raw keycode numbers for keys we'll use in generated "keys". - For example, we want to be able to check against these numbers in - the internal_keycodes reducer fragments, but due to a limitation in - MicroPython, we can't simply assign the `.code` attribute to - a function (which is what most internal KMK keys (including layer stuff) - are implemented as). Thus, we have to keep an external lookup table. - ''' - LCTRL = 0x01 - LSHIFT = 0x02 - LALT = 0x04 - LGUI = 0x08 - RCTRL = 0x10 - RSHIFT = 0x20 - RALT = 0x40 - RGUI = 0x80 - - KC_DF = 1050 - KC_MO = 1051 - KC_LM = 1052 - KC_LT = 1053 - KC_TG = 1054 - KC_TO = 1055 - KC_TT = 1056 - - KC_UC_MODE = 1109 - - KC_MACRO = 1110 - KC_MACRO_SLEEP_MS = 1111 - KC_TAP_DANCE = 1113 - - -# These shouldn't have all the fancy shenanigans Keycode allows -# such as no_press, because they modify KMK internal state in -# ways we need to tightly control. Thus, we can get away with -# a lighter-weight namedtuple implementation here -LayerKeycode = namedtuple('LayerKeycode', ('code', 'layer', 'kc')) -MacroSleepKeycode = namedtuple('MacroSleepKeycode', ('code', 'ms')) - - -class UnicodeModeKeycode(namedtuple('UnicodeModeKeycode', ('code', 'mode'))): - @staticmethod - def from_mode_const(mode): - return UnicodeModeKeycode(RawKeycodes.KC_UC_MODE, mode) - - class Keycode: - def __init__(self, code, has_modifiers=None, no_press=False, no_release=False): + def __init__( + self, + code, + has_modifiers=None, + no_press=False, + no_release=False, + on_press=handlers.default_pressed, + on_release=handlers.default_released, + meta=object(), + ): self.code = code self.has_modifiers = has_modifiers # cast to bool() in case we get a None value self.no_press = bool(no_press) self.no_release = bool(no_press) + self._on_press = on_press + self._on_release = on_release + self.meta = meta + def __call__(self, no_press=None, no_release=None): if no_press is None and no_release is None: return self @@ -119,8 +64,17 @@ class Keycode: def __repr__(self): return 'Keycode(code={}, has_modifiers={})'.format(self.code, self.has_modifiers) + def on_press(self, state, coord_int, coord_raw): + return self._on_press(self, state, KC, coord_int, coord_raw) + + def on_release(self, state, coord_int, coord_raw): + return self._on_release(self, state, KC, coord_int, coord_raw) + class ModifierKeycode(Keycode): + # FIXME this is atrocious to read. Please, please, please, strike down upon + # this with great vengeance and furious anger. + FAKE_CODE = -1 def __call__(self, modified_code=None, no_press=None, no_release=None): @@ -168,525 +122,460 @@ class ConsumerKeycode(Keycode): pass -class Macro: +def register_key_names(key, names=tuple()): # NOQA ''' - A special "key" which triggers a macro. + Names are globally unique. If a later key is created with + the same name as an existing entry in `KC`, it will overwrite + the existing entry. + + If a name entry is only a single letter, its entry in the KC + object will not be case-sensitive (meaning `names=('A',)` is + sufficient to create a key accessible by both `KC.A` and `KC.a`). ''' - code = RawKeycodes.KC_MACRO - def __init__(self, keydown=None, keyup=None): - self.keydown = keydown - self.keyup = keyup + for name in names: + ALL_KEYS[name] = key - def on_keydown(self): - return self.keydown() if self.keydown else None + if len(name) == 1: + ALL_KEYS[name.upper()] = key + ALL_KEYS[name.lower()] = key - def on_keyup(self): - return self.keyup() if self.keyup else None + return key -class TapDanceKeycode: - code = RawKeycodes.KC_TAP_DANCE +def make_key( + code=None, + names=tuple(), # NOQA + type=KEYCODE_SIMPLE, + **kwargs, +): + ''' + Create a new key, aliased by `names` in the KC lookup table. - def __init__(self, *codes): - self.codes = codes + If a code is not specified, the key is assumed to be a custom + internal key to be handled in a state callback rather than + sent directly to the OS. These codes will autoincrement. + + See register_key_names() for details on the assignment. + + All **kwargs are passed to the Keycode constructor + ''' + + global NEXT_AVAILABLE_KEYCODE + + if type == KEYCODE_SIMPLE: + constructor = Keycode + elif type == KEYCODE_MODIFIER: + constructor = ModifierKeycode + elif type == KEYCODE_CONSUMER: + constructor = ConsumerKeycode + else: + raise ValueError('Unrecognized key type') + + if code is None: + code = NEXT_AVAILABLE_KEYCODE + NEXT_AVAILABLE_KEYCODE += 1 + elif code >= FIRST_KMK_INTERNAL_KEYCODE: + # Try to ensure future auto-generated internal keycodes won't + # be overridden by continuing to +1 the sequence from the provided + # code + NEXT_AVAILABLE_KEYCODE = max(NEXT_AVAILABLE_KEYCODE, code + 1) + + key = constructor(code=code, **kwargs) + + register_key_names(key, names) + + return key -class KeycodeCategory(type): - @classmethod - def to_dict(cls): - ''' - MicroPython, for whatever reason (probably performance/memory) makes - __dict__ optional for ports. Unfortunately, at least the STM32 - (Pyboard) port is one such port. This reimplements a subset of - __dict__, limited to just keys we're likely to care about (though this - could be opened up further later). - ''' - - hidden = ('to_dict', 'recursive_dict', 'contains') - return AttrDict({ - key: getattr(cls, key) - for key in dir(cls) - if not key.startswith('_') and key not in hidden - }) - - @classmethod - def recursive_dict(cls): - ''' - to_dict() executed recursively all the way down a tree - ''' - ret = cls.to_dict() - - for key, val in ret.items(): - try: - nested_ret = val.recursive_dict() - except (AttributeError, NameError): - continue - - ret[key] = nested_ret - - return ret - - @classmethod - def contains(cls, kc): - ''' - Emulates the 'in' operator for keycode groupings, given MicroPython's - lack of support for metaclasses (meaning implementing 'in' for - uninstantiated classes, such as these, is largely not possible). Not - super useful in most cases, but does allow for sanity checks like - - ```python - assert Keycodes.Modifiers.contains(requested_key) - ``` - - This is not bulletproof due to how HID codes are defined (there is - overlap). Keycodes.Common.KC_A, for example, is equal in value to - Keycodes.Modifiers.KC_LALT, but it can still prevent silly mistakes - like trying to use, say, Keycodes.Common.KC_Q as a modifier. - - This is recursive across subgroups, enabling stuff like: - - ```python - assert Keycodes.contains(requested_key) - ``` - - To ensure that a valid keycode has been requested to begin with. Again, - not bulletproof, but adds at least some cushion to stuff that would - otherwise cause AttributeErrors and crash the keyboard. - ''' - subcategories = ( - category for category in cls.to_dict().values() - # Disgusting, but since `cls.__bases__` isn't implemented in MicroPython, - # I resort to a less foolproof inheritance check that should still ignore - # strings and other stupid stuff (we don't want to iterate over __doc__, - # for example), but include nested classes. - # - # One huge lesson in this project is that uninstantiated classes are hard... - # and four times harder when the implementation of Python is half-baked. - if isinstance(category, type) - ) - - if any( - kc == _kc - for name, _kc in cls.to_dict().items() - if name.startswith('KC_') - ): - return True - - return any(sc.contains(kc) for sc in subcategories) +def make_mod_key(*args, **kwargs): + return make_key(*args, **kwargs, type=KEYCODE_MODIFIER) -class Modifiers(KeycodeCategory): - KC_LCTRL = KC_LCTL = ModifierKeycode(RawKeycodes.LCTRL) - KC_LSHIFT = KC_LSFT = ModifierKeycode(RawKeycodes.LSHIFT) - KC_LALT = ModifierKeycode(RawKeycodes.LALT) - KC_LGUI = KC_LCMD = KC_LWIN = ModifierKeycode(RawKeycodes.LGUI) - KC_RCTRL = KC_RCTL = ModifierKeycode(RawKeycodes.RCTRL) - KC_RSHIFT = KC_RSFT = ModifierKeycode(RawKeycodes.RSHIFT) - KC_RALT = ModifierKeycode(RawKeycodes.RALT) - KC_RGUI = KC_RCMD = KC_RWIN = ModifierKeycode(RawKeycodes.RGUI) +def make_shifted_key(target_name, names=tuple()): # NOQA + key = KC.LSFT(ALL_KEYS[target_name]) - KC_MEH = KC_LSHIFT(KC_LALT(KC_LCTRL)) - KC_HYPR = KC_HYPER = KC_MEH(KC_LGUI) + register_key_names(key, names) + + return key -class Common(KeycodeCategory): - KC_A = Keycode(4) - KC_B = Keycode(5) - KC_C = Keycode(6) - KC_D = Keycode(7) - KC_E = Keycode(8) - KC_F = Keycode(9) - KC_G = Keycode(10) - KC_H = Keycode(11) - KC_I = Keycode(12) - KC_J = Keycode(13) - KC_K = Keycode(14) - KC_L = Keycode(15) - KC_M = Keycode(16) - KC_N = Keycode(17) - KC_O = Keycode(18) - KC_P = Keycode(19) - KC_Q = Keycode(20) - KC_R = Keycode(21) - KC_S = Keycode(22) - KC_T = Keycode(23) - KC_U = Keycode(24) - KC_V = Keycode(25) - KC_W = Keycode(26) - KC_X = Keycode(27) - KC_Y = Keycode(28) - KC_Z = Keycode(29) - - # Aliases to play nicely with AttrDict, since KC.1 isn't a valid - # attribute key in Python, but KC.N1 is - KC_1 = KC_N1 = Keycode(30) - KC_2 = KC_N2 = Keycode(31) - KC_3 = KC_N3 = Keycode(32) - KC_4 = KC_N4 = Keycode(33) - KC_5 = KC_N5 = Keycode(34) - KC_6 = KC_N6 = Keycode(35) - KC_7 = KC_N7 = Keycode(36) - KC_8 = KC_N8 = Keycode(37) - KC_9 = KC_N9 = Keycode(38) - KC_0 = KC_N0 = Keycode(39) - - KC_ENTER = KC_ENT = Keycode(40) - KC_ESCAPE = KC_ESC = Keycode(41) - KC_BACKSPACE = KC_BSPC = KC_BKSP = Keycode(42) - KC_TAB = Keycode(43) - KC_SPACE = KC_SPC = Keycode(44) - KC_MINUS = KC_MINS = Keycode(45) - KC_EQUAL = KC_EQL = Keycode(46) - KC_LBRACKET = KC_LBRC = Keycode(47) - KC_RBRACKET = KC_RBRC = Keycode(48) - KC_BACKSLASH = KC_BSLASH = KC_BSLS = Keycode(49) - KC_NONUS_HASH = KC_NUHS = Keycode(50) - KC_NONUS_BSLASH = KC_NUBS = Keycode(100) - KC_SEMICOLON = KC_SCOLON = KC_SCLN = Keycode(51) - KC_QUOTE = KC_QUOT = Keycode(52) - KC_GRAVE = KC_GRV = KC_ZKHK = Keycode(53) - KC_COMMA = KC_COMM = Keycode(54) - KC_DOT = Keycode(55) - KC_SLASH = KC_SLSH = Keycode(56) +def make_consumer_key(*args, **kwargs): + return make_key(*args, **kwargs, type=KEYCODE_CONSUMER) -class ShiftedKeycodes(KeycodeCategory): - KC_TILDE = KC_TILD = Modifiers.KC_LSHIFT(Common.KC_GRAVE) - KC_EXCLAIM = KC_EXLM = Modifiers.KC_LSHIFT(Common.KC_1) - KC_AT = Modifiers.KC_LSHIFT(Common.KC_2) - KC_HASH = Modifiers.KC_LSHIFT(Common.KC_3) - KC_DOLLAR = KC_DLR = Modifiers.KC_LSHIFT(Common.KC_4) - KC_PERCENT = KC_PERC = Modifiers.KC_LSHIFT(Common.KC_5) - KC_CIRCUMFLEX = KC_CIRC = Modifiers.KC_LSHIFT(Common.KC_6) # The ^ Symbol - KC_AMPERSAND = KC_AMPR = Modifiers.KC_LSHIFT(Common.KC_7) - KC_ASTERISK = KC_ASTR = Modifiers.KC_LSHIFT(Common.KC_8) - KC_LEFT_PAREN = KC_LPRN = Modifiers.KC_LSHIFT(Common.KC_9) - KC_RIGHT_PAREN = KC_RPRN = Modifiers.KC_LSHIFT(Common.KC_0) - KC_UNDERSCORE = KC_UNDS = Modifiers.KC_LSHIFT(Common.KC_MINUS) - KC_PLUS = Modifiers.KC_LSHIFT(Common.KC_EQUAL) - KC_LEFT_CURLY_BRACE = KC_LCBR = Modifiers.KC_LSHIFT(Common.KC_LBRACKET) - KC_RIGHT_CURLY_BRACE = KC_RCBR = Modifiers.KC_LSHIFT(Common.KC_RBRACKET) - KC_PIPE = Modifiers.KC_LSHIFT(Common.KC_BACKSLASH) - KC_COLON = KC_COLN = Modifiers.KC_LSHIFT(Common.KC_SEMICOLON) - KC_DOUBLE_QUOTE = KC_DQUO = KC_DQT = Modifiers.KC_LSHIFT(Common.KC_QUOTE) - KC_LEFT_ANGLE_BRACKET = KC_LABK = Modifiers.KC_LSHIFT(Common.KC_COMMA) - KC_RIGHT_ANGLE_BRACKET = KC_RABK = Modifiers.KC_LSHIFT(Common.KC_DOT) - KC_QUESTION = KC_QUES = Modifiers.KC_LSHIFT(Common.KC_SLSH) +# Argumented keys are implicitly internal, so auto-gen of code +# is almost certainly the best plan here +def make_argumented_key( + validator=lambda *validator_args, **validator_kwargs: object(), + *constructor_args, + **constructor_kwargs, +): + def _argumented_key(*user_args, **user_kwargs): + meta = validator(*user_args, **user_kwargs) + + if meta: + return Keycode( + meta=meta, + *constructor_args, + **constructor_kwargs, + ) + else: + raise ValueError( + 'Argumented key validator failed for unknown reasons. ' + 'This may not be the keymap\'s fault, as a more specific error ' + 'should have been raised.', + ) -class FunctionKeys(KeycodeCategory): - KC_F1 = Keycode(58) - KC_F2 = Keycode(59) - KC_F3 = Keycode(60) - KC_F4 = Keycode(61) - KC_F5 = Keycode(62) - KC_F6 = Keycode(63) - KC_F7 = Keycode(64) - KC_F8 = Keycode(65) - KC_F9 = Keycode(66) - KC_F10 = Keycode(67) - KC_F11 = Keycode(68) - KC_F12 = Keycode(69) - KC_F13 = Keycode(104) - KC_F14 = Keycode(105) - KC_F15 = Keycode(106) - KC_F16 = Keycode(107) - KC_F17 = Keycode(108) - KC_F18 = Keycode(109) - KC_F19 = Keycode(110) - KC_F20 = Keycode(111) - KC_F21 = Keycode(112) - KC_F22 = Keycode(113) - KC_F23 = Keycode(114) - KC_F24 = Keycode(115) +# Modifiers +make_mod_key(code=0x01, names=('LEFT_CONTROL', 'LCTRL', 'LCTL')) +make_mod_key(code=0x02, names=('LEFT_SHIFT', 'LSHIFT', 'LSFT')) +make_mod_key(code=0x04, names=('LEFT_ALT', 'LALT')) +make_mod_key(code=0x08, names=('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')) +make_mod_key(code=0x10, names=('RIGHT_CONTROL', 'RCTRL', 'RCTL')) +make_mod_key(code=0x20, names=('RIGHT_SHIFT', 'RSHIFT', 'RSFT')) +make_mod_key(code=0x40, names=('RIGHT_ALT', 'RALT')) +make_mod_key(code=0x80, names=('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')) +# MEH = LCTL | LALT | LSFT +make_mod_key(code=0x07, names=('MEH',)) +# HYPR = LCTL | LALT | LSFT | LGUI +make_mod_key(code=0x0F, names=('HYPER', 'HYPR')) +# Basic ASCII letters +make_key(code=4, names=('A',)) +make_key(code=5, names=('B',)) +make_key(code=6, names=('C',)) +make_key(code=7, names=('D',)) +make_key(code=8, names=('E',)) +make_key(code=9, names=('F',)) +make_key(code=10, names=('G',)) +make_key(code=11, names=('H',)) +make_key(code=12, names=('I',)) +make_key(code=13, names=('J',)) +make_key(code=14, names=('K',)) +make_key(code=15, names=('L',)) +make_key(code=16, names=('M',)) +make_key(code=17, names=('N',)) +make_key(code=18, names=('O',)) +make_key(code=19, names=('P',)) +make_key(code=20, names=('Q',)) +make_key(code=21, names=('R',)) +make_key(code=22, names=('S',)) +make_key(code=23, names=('T',)) +make_key(code=24, names=('U',)) +make_key(code=25, names=('V',)) +make_key(code=26, names=('W',)) +make_key(code=27, names=('X',)) +make_key(code=28, names=('Y',)) +make_key(code=29, names=('Z',)) -class NavAndLocks(KeycodeCategory): - KC_CAPS_LOCK = KC_CLCK = KC_CAPS = Keycode(57) - KC_LOCKING_CAPS = KC_LCAP = Keycode(130) - KC_PSCREEN = KC_PSCR = Keycode(70) - KC_SCROLLLOCK = KC_SLCK = Keycode(71) - KC_LOCKING_SCROLL = KC_LSCRL = Keycode(132) - KC_PAUSE = KC_PAUS = KC_BRK = Keycode(72) - KC_INSERT = KC_INS = Keycode(73) - KC_HOME = Keycode(74) - KC_PGUP = Keycode(75) - KC_DELETE = KC_DEL = Keycode(76) - KC_END = Keycode(77) - KC_PGDOWN = KC_PGDN = Keycode(78) - KC_RIGHT = KC_RGHT = Keycode(79) - KC_LEFT = Keycode(80) - KC_DOWN = Keycode(81) - KC_UP = Keycode(82) +# Numbers +# Aliases to play nicely with AttrDict, since KC.1 isn't a valid +# attribute key in Python, but KC.N1 is +make_key(code=30, names=('1', 'N1')) +make_key(code=31, names=('2', 'N2')) +make_key(code=32, names=('3', 'N3')) +make_key(code=33, names=('4', 'N4')) +make_key(code=34, names=('5', 'N5')) +make_key(code=35, names=('6', 'N6')) +make_key(code=36, names=('7', 'N7')) +make_key(code=37, names=('8', 'N8')) +make_key(code=38, names=('9', 'N9')) +make_key(code=39, names=('0', 'N0')) +# More ASCII standard keys +make_key(code=40, names=('ENTER', 'ENT', "\n")) +make_key(code=41, names=('ESCAPE', 'ESC')) +make_key(code=42, names=('BACKSPACE', 'BSPC', 'BKSP')) +make_key(code=43, names=('TAB', "\t")) +make_key(code=44, names=('SPACE', 'SPC', ' ')) +make_key(code=45, names=('MINUS', 'MINS', '-')) +make_key(code=46, names=('EQUAL', 'EQL', '=')) +make_key(code=47, names=('LBRACKET', 'LBRC', '[')) +make_key(code=48, names=('RBRACKET', 'RBRC', ']')) +make_key(code=49, names=('BACKSLASH', 'BSLASH', 'BSLS', "\\")) +make_key(code=51, names=('SEMICOLON', 'SCOLON', 'SCLN', ';')) +make_key(code=52, names=('QUOTE', 'QUOT', "'")) +make_key(code=53, names=('GRAVE', 'GRV', 'ZKHK', '`')) +make_key(code=54, names=('COMMA', 'COMM', ',')) +make_key(code=55, names=('DOT', '.')) +make_key(code=56, names=('SLASH', 'SLSH')) -class Numpad(KeycodeCategory): - KC_NUMLOCK = KC_NLCK = Keycode(83) - KC_LOCKING_NUM = KC_LNUM = Keycode(131) - KC_KP_SLASH = KC_PSLS = Keycode(84) - KC_KP_ASTERIK = KC_PAST = Keycode(85) - KC_KP_MINUS = KC_PMNS = Keycode(86) - KC_KP_PLUS = KC_PPLS = Keycode(87) - KC_KP_ENTER = KC_PENT = Keycode(88) - KC_KP_1 = KC_P1 = Keycode(89) - KC_KP_2 = KC_P2 = Keycode(90) - KC_KP_3 = KC_P3 = Keycode(91) - KC_KP_4 = KC_P4 = Keycode(92) - KC_KP_5 = KC_P5 = Keycode(93) - KC_KP_6 = KC_P6 = Keycode(94) - KC_KP_7 = KC_P7 = Keycode(95) - KC_KP_8 = KC_P8 = Keycode(96) - KC_KP_9 = KC_P9 = Keycode(97) - KC_KP_0 = KC_P0 = Keycode(98) - KC_KP_DOT = KC_PDOT = Keycode(99) - KC_KP_EQUAL = KC_PEQL = Keycode(103) - KC_KP_COMMA = KC_PCMM = Keycode(133) - KC_KP_EQUAL_AS400 = Keycode(134) +# Function Keys +make_key(code=58, names=('F1',)) +make_key(code=59, names=('F2',)) +make_key(code=60, names=('F3',)) +make_key(code=61, names=('F4',)) +make_key(code=62, names=('F5',)) +make_key(code=63, names=('F6',)) +make_key(code=64, names=('F7',)) +make_key(code=65, names=('F8',)) +make_key(code=66, names=('F9',)) +make_key(code=67, names=('F10',)) +make_key(code=68, names=('F10',)) +make_key(code=69, names=('F10',)) +make_key(code=104, names=('F13',)) +make_key(code=105, names=('F14',)) +make_key(code=106, names=('F15',)) +make_key(code=107, names=('F16',)) +make_key(code=108, names=('F17',)) +make_key(code=109, names=('F18',)) +make_key(code=110, names=('F19',)) +make_key(code=111, names=('F20',)) +make_key(code=112, names=('F21',)) +make_key(code=113, names=('F22',)) +make_key(code=114, names=('F23',)) +make_key(code=115, names=('F24',)) +# Lock Keys, Navigation, etc. +make_key(code=57, names=('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')) +# FIXME: Investigate whether this key actually works, and +# uncomment when/if it does. +# make_key(code=130, names=('LOCKING_CAPS', 'LCAP')) +make_key(code=70, names=('PRINT_SCREEN', 'PSCREEN', 'PSCR')) +make_key(code=71, names=('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')) +# FIXME: Investigate whether this key actually works, and +# uncomment when/if it does. +# make_key(code=132, names=('LOCKING_SCROLL', 'LSCRL')) +make_key(code=72, names=('PAUSE', 'PAUS', 'BRK')) +make_key(code=73, names=('INSERT', 'INS')) +make_key(code=74, names=('HOME',)) +make_key(code=75, names=('PGUP',)) +make_key(code=76, names=('DELETE', 'DEL')) +make_key(code=77, names=('END',)) +make_key(code=78, names=('PGDOWN', 'PGDN')) +make_key(code=79, names=('RIGHT', 'RGHT')) +make_key(code=80, names=('LEFT',)) +make_key(code=81, names=('DOWN',)) +make_key(code=82, names=('UP',)) -class International(KeycodeCategory): - KC_INT1 = KC_RO = Keycode(135) - KC_INT2 = KC_KANA = Keycode(136) - KC_INT3 = KC_JYEN = Keycode(137) - KC_INT4 = KC_HENK = Keycode(138) - KC_INT5 = KC_MHEN = Keycode(139) - KC_INT6 = Keycode(140) - KC_INT7 = Keycode(141) - KC_INT8 = Keycode(142) - KC_INT9 = Keycode(143) - KC_LANG1 = KC_HAEN = Keycode(144) - KC_LANG2 = KC_HAEJ = Keycode(145) - KC_LANG3 = Keycode(146) - KC_LANG4 = Keycode(147) - KC_LANG5 = Keycode(148) - KC_LANG6 = Keycode(149) - KC_LANG7 = Keycode(150) - KC_LANG8 = Keycode(151) - KC_LANG9 = Keycode(152) +# Numpad +make_key(code=83, names=('NUM_LOCK', 'NUMLOCK', 'NLCK')) +# FIXME: Investigate whether this key actually works, and +# uncomment when/if it does. +# make_key(code=131, names=('LOCKING_NUM', 'LNUM')) +make_key(code=84, names=('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')) +make_key(code=85, names=('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')) +make_key(code=86, names=('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')) +make_key(code=87, names=('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')) +make_key(code=88, names=('KP_ENTER', 'NUMPAD_ENTER', 'PENT')) +make_key(code=89, names=('KP_1', 'P1', 'NUMPAD_1')) +make_key(code=90, names=('KP_2', 'P2', 'NUMPAD_2')) +make_key(code=91, names=('KP_3', 'P3', 'NUMPAD_3')) +make_key(code=92, names=('KP_4', 'P4', 'NUMPAD_4')) +make_key(code=93, names=('KP_5', 'P5', 'NUMPAD_5')) +make_key(code=94, names=('KP_6', 'P6', 'NUMPAD_6')) +make_key(code=95, names=('KP_7', 'P7', 'NUMPAD_7')) +make_key(code=96, names=('KP_8', 'P8', 'NUMPAD_8')) +make_key(code=97, names=('KP_9', 'P9', 'NUMPAD_9')) +make_key(code=98, names=('KP_0', 'P0', 'NUMPAD_0')) +make_key(code=99, names=('KP_DOT', 'PDOT', 'NUMPAD_DOT')) +make_key(code=103, names=('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')) +make_key(code=133, names=('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')) +make_key(code=134, names=('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')) +# Making life better for folks on tiny keyboards especially: exposes +# the "shifted" keys as raw keys. Under the hood we're still +# sending Shift+(whatever key is normally pressed) to get these, so +# for example `KC_AT` will hold shift and press 2. +make_shifted_key('GRAVE', names=('TILDE', 'TILD', '~')) +make_shifted_key('1', names=('EXCLAIM', 'EXLM', '!')) +make_shifted_key('2', names=('AT', '@')) +make_shifted_key('3', names=('HASH', 'POUND', '#')) +make_shifted_key('4', names=('DOLLAR', 'DLR', '$')) +make_shifted_key('5', names=('PERCENT', 'PERC', '%')) +make_shifted_key('6', names=('CIRCUMFLEX', 'CIRC', '^')) +make_shifted_key('7', names=('AMPERSAND', 'AMPR', '&')) +make_shifted_key('8', names=('ASTERISK', 'ASTR', '*')) +make_shifted_key('9', names=('LEFT_PAREN', 'LPRN', '(')) +make_shifted_key('0', names=('RIGHT_PAREN', 'RPRN', ')')) +make_shifted_key('MINUS', names=('UNDERSCORE', 'UNDS', '_')) +make_shifted_key('EQUAL', names=('PLUS', '+')) +make_shifted_key('LBRACKET', names=('LEFT_CURLY_BRACE', 'LCBR', '{')) +make_shifted_key('RBRACKET', names=('RIGHT_CURLY_BRACE', 'RCBR', '}')) +make_shifted_key('BACKSLASH', names=('PIPE', '|')) +make_shifted_key('SEMICOLON', names=('COLON', 'COLN', ':')) +make_shifted_key('QUOTE', names=('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')) +make_shifted_key('COMMA', names=('LEFT_ANGLE_BRACKET', 'LABK', '<')) +make_shifted_key('DOT', names=('RIGHT_ANGLE_BRACKET', 'RABK', '>')) +make_shifted_key('SLSH', names=('QUESTION', 'QUES', '?')) -class Misc(KeycodeCategory): - KC_APPLICATION = KC_APP = ConsumerKeycode(101) - KC_POWER = ConsumerKeycode(102) - KC_EXECUTE = KC_EXEC = ConsumerKeycode(116) - KC_SYSTEM_POWER = KC_PWR = ConsumerKeycode(165) - KC_SYSTEM_SLEEP = KC_SLEP = ConsumerKeycode(166) - KC_SYSTEM_WAKE = KC_WAKE = ConsumerKeycode(167) - KC_HELP = ConsumerKeycode(117) - KC_MENU = ConsumerKeycode(118) - KC_SELECT = KC_SLCT = ConsumerKeycode(119) - KC_STOP = ConsumerKeycode(120) - KC_AGAIN = KC_AGIN = ConsumerKeycode(121) - KC_UNDO = ConsumerKeycode(122) - KC_CUT = ConsumerKeycode(123) - KC_COPY = ConsumerKeycode(124) - KC_PASTE = KC_PSTE = ConsumerKeycode(125) - KC_FIND = ConsumerKeycode(126) - KC_ALT_ERASE = KC_ERAS = ConsumerKeycode(153) - KC_SYSREQ = ConsumerKeycode(154) - KC_CANCEL = ConsumerKeycode(155) - KC_CLEAR = KC_CLR = ConsumerKeycode(156) - KC_PRIOR = ConsumerKeycode(157) - KC_RETURN = ConsumerKeycode(158) - KC_SEPERATOR = ConsumerKeycode(159) - KC_OUT = ConsumerKeycode(160) - KC_OPER = ConsumerKeycode(161) - KC_CLEAR_AGAIN = ConsumerKeycode(162) - KC_CRSEL = ConsumerKeycode(163) - KC_EXSEL = ConsumerKeycode(164) - KC_MAIL = ConsumerKeycode(177) - KC_CALCULATOR = KC_CALC = ConsumerKeycode(178) - KC_MY_COMPUTER = KC_MYCM = ConsumerKeycode(179) - KC_WWW_SEARCH = KC_WSCH = ConsumerKeycode(180) - KC_WWW_HOME = KC_WHOM = ConsumerKeycode(181) - KC_WWW_BACK = KC_WBAK = ConsumerKeycode(182) - KC_WWW_FORWARD = KC_WFWD = ConsumerKeycode(183) - KC_WWW_STOP = KC_WSTP = ConsumerKeycode(184) - KC_WWW_REFRESH = KC_WREF = ConsumerKeycode(185) - KC_WWW_FAVORITES = KC_WFAV = ConsumerKeycode(186) +# International +make_key(code=50, names=('NONUS_HASH', 'NUHS')) +make_key(code=100, names=('NONUS_BSLASH', 'NUBS')) +make_key(code=135, names=('INT1', 'RO')) +make_key(code=136, names=('INT2', 'KANA')) +make_key(code=137, names=('INT3', 'JYEN')) +make_key(code=138, names=('INT4', 'HENK')) +make_key(code=139, names=('INT5', 'MHEN')) +make_key(code=140, names=('INT6',)) +make_key(code=141, names=('INT7',)) +make_key(code=142, names=('INT8',)) +make_key(code=143, names=('INT9',)) +make_key(code=144, names=('LANG1', 'HAEN')) +make_key(code=145, names=('LANG2', 'HAEJ')) +make_key(code=146, names=('LANG3',)) +make_key(code=147, names=('LANG4',)) +make_key(code=148, names=('LANG5',)) +make_key(code=149, names=('LANG6',)) +make_key(code=150, names=('LANG7',)) +make_key(code=151, names=('LANG8',)) +make_key(code=152, names=('LANG9',)) -class Media(KeycodeCategory): - # I believe QMK used these double-underscore codes for MacOS - # support or something. I have no idea, but modern MacOS supports - # PC volume keys so I really don't care that these codes are the - # same as below. If bugs arise, these codes may need to change. - KC__MUTE = ConsumerKeycode(226) - KC__VOLUP = ConsumerKeycode(233) - KC__VOLDOWN = ConsumerKeycode(234) +# Consumer ("media") keys. Most known keys aren't supported here. A much +# longer list used to exist in this file, but the codes were almost certainly +# incorrect, conflicting with each other, or otherwise "weird". We'll add them +# back in piecemeal as needed. PRs welcome. +# +# A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php +# Note that currently we only have the PC codes. Recent MacOS versions seem to +# support PC media keys, so I don't know how much value we would get out of +# adding the old Apple-specific consumer codes, but again, PRs welcome if the +# lack of them impacts you. +make_consumer_key(code=226, names=('AUDIO_MUTE', 'MUTE')) # 0xE2 +make_consumer_key(code=233, names=('AUDIO_VOL_UP', 'VOLU')) # 0xE9 +make_consumer_key(code=234, names=('AUDIO_VOL_DOWN', 'VOLD')) # 0xEA +make_consumer_key(code=181, names=('MEDIA_NEXT_TRACK', 'MNXT')) # 0xB5 +make_consumer_key(code=182, names=('MEDIA_PREV_TRACK', 'MPRV')) # 0xB6 +make_consumer_key(code=183, names=('MEDIA_STOP', 'MSTP')) # 0xB7 +make_consumer_key(code=205, names=('MEDIA_PLAY_PAUSE', 'MPLY')) # 0xCD (this may not be right) +make_consumer_key(code=184, names=('MEDIA_EJECT', 'EJCT')) # 0xB8 +make_consumer_key(code=179, names=('MEDIA_FAST_FORWARD', 'MFFD')) # 0xB3 +make_consumer_key(code=180, names=('MEDIA_REWIND', 'MRWD')) # 0xB4 - KC_AUDIO_MUTE = KC_MUTE = ConsumerKeycode(226) # 0xE2 - KC_AUDIO_VOL_UP = KC_VOLU = ConsumerKeycode(233) # 0xE9 - KC_AUDIO_VOL_DOWN = KC_VOLD = ConsumerKeycode(234) # 0xEA - KC_MEDIA_NEXT_TRACK = KC_MNXT = ConsumerKeycode(181) # 0xB5 - KC_MEDIA_PREV_TRACK = KC_MPRV = ConsumerKeycode(182) # 0xB6 - KC_MEDIA_STOP = KC_MSTP = ConsumerKeycode(183) # 0xB7 - KC_MEDIA_PLAY_PAUSE = KC_MPLY = ConsumerKeycode(205) # 0xCD (this may not be right) - KC_MEDIA_EJECT = KC_EJCT = ConsumerKeycode(184) # 0xB8 - KC_MEDIA_FAST_FORWARD = KC_MFFD = ConsumerKeycode(179) # 0xB3 - KC_MEDIA_REWIND = KC_MRWD = ConsumerKeycode(180) # 0xB4 +# Internal, diagnostic, or auxiliary/enhanced keys - -class KMK(KeycodeCategory): - KC_RESET = Keycode(1000) - KC_DEBUG = Keycode(1001) - KC_GESC = Keycode(1002) - KC_LSPO = Keycode(1003) - KC_RSPC = Keycode(1004) - KC_LEAD = Keycode(1005) - KC_LOCK = Keycode(1006) - KC_NO = Keycode(1107) - KC_TRANSPARENT = KC_TRNS = Keycode(1108) - KC_DEBUG = KC_DBG = Keycode(1112) - - @staticmethod - def KC_UC_MODE(mode): - ''' - Set any Unicode Mode at runtime (allows the same keymap's unicode - sequences to work across all supported platforms) - ''' - return UnicodeModeKeycode.from_mode_const(mode) - - KC_UC_MODE_NOOP = KC_UC_DISABLE = UnicodeModeKeycode.from_mode_const(UnicodeMode.NOOP) - KC_UC_MODE_LINUX = KC_UC_MODE_IBUS = UnicodeModeKeycode.from_mode_const(UnicodeMode.IBUS) - KC_UC_MODE_MACOS = KC_UC_MODE_OSX = KC_UC_MODE_RALT = UnicodeModeKeycode.from_mode_const( - UnicodeMode.RALT, +# NO and TRNS are functionally identical in how they (don't) mutate +# the state, but are tracked semantically separately, so create +# two keys with the exact same functionality +for names in (('NO',), ('TRANSPARENT', 'TRNS')): + make_key( + names=names, + on_press=handlers.passthrough, + on_release=handlers.passthrough, ) - KC_UC_MODE_WINC = UnicodeModeKeycode.from_mode_const(UnicodeMode.WINC) - @staticmethod - def KC_MACRO_SLEEP_MS(ms): - return MacroSleepKeycode(RawKeycodes.KC_MACRO_SLEEP_MS, ms) +make_key(names=('RESET',), on_press=handlers.reset) +make_key(names=('BOOTLOADER',), on_press=handlers.bootloader) +make_key(names=('DEBUG', 'DBG'), on_press=handlers.debug_pressed, on_release=handlers.passthrough) - @staticmethod - def KC_TAP_DANCE(*args): - return TapDanceKeycode(*args) +make_key(names=('GESC',), on_press=handlers.gesc_pressed, on_release=handlers.gesc_released) +make_key( + names=('LEADER', 'LEAD'), + on_press=handlers.leader_pressed, + on_release=handlers.leader_released, +) -KMK.KC_TD = KMK.KC_TAP_DANCE - - -class Layers(KeycodeCategory): - @staticmethod - def KC_DF(layer): - return LayerKeycode(RawKeycodes.KC_DF, layer, KC.NO) - - @staticmethod - def KC_MO(layer): - return LayerKeycode(RawKeycodes.KC_MO, layer, KC.NO) - - @staticmethod - def KC_LM(layer, kc): - return LayerKeycode(RawKeycodes.KC_LM, layer, kc) - - @staticmethod - def KC_LT(layer, kc): - return LayerKeycode(RawKeycodes.KC_LT, layer, kc) - - @staticmethod - def KC_TG(layer): - return LayerKeycode(RawKeycodes.KC_TG, layer, KC.NO) - - @staticmethod - def KC_TO(layer): - return LayerKeycode(RawKeycodes.KC_TO, layer, KC.NO) - - @staticmethod - def KC_TT(layer): - return LayerKeycode(RawKeycodes.KC_TT, layer, KC.NO) - - -class Keycodes(KeycodeCategory): +def layer_key_validator(layer, kc=None): ''' - A massive grouping of keycodes - - Some of these are from http://www.freebsddiary.org/APC/usb_hid_usages.php, - one of the most useful pages on the interwebs for HID stuff, apparently. + Validates the syntax (but not semantics) of a layer key call. We won't + have access to the keymap here, so we can't verify much of anything useful + here (like whether the target layer actually exists). The spirit of this + existing is mostly that Python will catch extraneous args/kwargs and error + out. ''' - _groupings = [ - 'Modifiers', - 'Common', - 'ShiftedKeycodes', - 'FunctionKeys', - 'NavAndLocks', - 'Numpad', - 'International', - 'Misc', - 'Media', - 'KMK', - 'Layers', - ] - - Modifiers = Modifiers - Common = Common - ShiftedKeycodes = ShiftedKeycodes - FunctionKeys = FunctionKeys - NavAndLocks = NavAndLocks - Numpad = Numpad - International = International - Misc = Misc - Media = Media - KMK = KMK - Layers = Layers + return LayerKeyMeta(layer=layer, kc=kc) -class LazyKC: - def __init__(self): - self.cache = {} - - def __getattr__(self, attr): - if attr in self.cache: - return self.cache[attr] - - for grouping in Keycodes._groupings: - grouping_cls = getattr(Keycodes, grouping) - - try: - found = getattr(grouping_cls, 'KC_{}'.format(attr)) - self.cache[attr] = found - return found - except AttributeError: - continue - - raise AttributeError(attr) +# Layers +make_argumented_key( + validator=layer_key_validator, + names=('MO',), + on_press=layers.mo_pressed, + on_release=layers.mo_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('DF',), + on_press=layers.df_pressed, + on_release=layers.df_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('LM',), + on_press=layers.lm_pressed, + on_release=layers.lm_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('LT',), + on_press=layers.lt_pressed, + on_release=layers.lt_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('TG',), + on_press=layers.tg_pressed, + on_release=layers.tg_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('TO',), + on_press=layers.to_pressed, + on_release=layers.to_released, +) +make_argumented_key( + validator=layer_key_validator, + names=('TT',), + on_press=layers.tt_pressed, + on_release=layers.tt_released, +) -KC = LazyKC() +def key_seq_sleep_validator(ms): + return KeySeqSleepMeta(ms) -char_lookup = { - "\n": Common.KC_ENTER, - "\t": Common.KC_TAB, - ' ': Common.KC_SPACE, - '-': Common.KC_MINUS, - '=': Common.KC_EQUAL, - '[': Common.KC_LBRACKET, - ']': Common.KC_RBRACKET, - "\\": Common.KC_BACKSLASH, - ';': Common.KC_SEMICOLON, - "'": Common.KC_QUOTE, - '`': Common.KC_GRAVE, - ',': Common.KC_COMMA, - '.': Common.KC_DOT, - '~': ShiftedKeycodes.KC_TILDE, - '!': ShiftedKeycodes.KC_EXCLAIM, - '@': ShiftedKeycodes.KC_AT, - '#': ShiftedKeycodes.KC_HASH, - '$': ShiftedKeycodes.KC_DOLLAR, - '%': ShiftedKeycodes.KC_PERCENT, - '^': ShiftedKeycodes.KC_CIRCUMFLEX, - '&': ShiftedKeycodes.KC_AMPERSAND, - '*': ShiftedKeycodes.KC_ASTERISK, - '(': ShiftedKeycodes.KC_LEFT_PAREN, - ')': ShiftedKeycodes.KC_RIGHT_PAREN, - '_': ShiftedKeycodes.KC_UNDERSCORE, - '+': ShiftedKeycodes.KC_PLUS, - '{': ShiftedKeycodes.KC_LEFT_CURLY_BRACE, - '}': ShiftedKeycodes.KC_RIGHT_CURLY_BRACE, - '|': ShiftedKeycodes.KC_PIPE, - ':': ShiftedKeycodes.KC_COLON, - '"': ShiftedKeycodes.KC_DOUBLE_QUOTE, - '<': ShiftedKeycodes.KC_LEFT_ANGLE_BRACKET, - '>': ShiftedKeycodes.KC_RIGHT_ANGLE_BRACKET, - '?': ShiftedKeycodes.KC_QUESTION, -} + +# A dummy key to trigger a sleep_ms call in a sequence of other keys in a +# simple sequence macro. +make_argumented_key( + validator=key_seq_sleep_validator, + names=('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'), + on_press=handlers.sleep_pressed, +) + + +# Switch unicode modes at runtime +make_key( + names=('UC_MODE_NOOP', 'UC_DISABLE'), + meta=UnicodeModeKeyMeta(UnicodeMode.NOOP), + on_press=handlers.uc_mode_pressed, +) +make_key( + names=('UC_MODE_LINUX', 'UC_MODE_IBUS'), + meta=UnicodeModeKeyMeta(UnicodeMode.IBUS), + on_press=handlers.uc_mode_pressed, +) +make_key( + names=('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'), + meta=UnicodeModeKeyMeta(UnicodeMode.RALT), + on_press=handlers.uc_mode_pressed, +) +make_key( + names=('UC_MODE_WINC',), + meta=UnicodeModeKeyMeta(UnicodeMode.WINC), + on_press=handlers.uc_mode_pressed, +) + + +def unicode_mode_key_validator(mode): + return UnicodeModeKeyMeta(mode) + + +make_argumented_key( + validator=unicode_mode_key_validator, + names=('UC_MODE',), + on_press=handlers.uc_mode_pressed, +) + + +# Tap Dance +make_argumented_key( + validator=lambda *codes: TapDanceKeyMeta(codes), + names=('TAP_DANCE', 'TD'), + on_press=handlers.td_pressed, + on_release=handlers.td_released, +) diff --git a/kmk/macros/rotary_encoder.py b/kmk/macros/rotary_encoder.py deleted file mode 100644 index 68abc8e..0000000 --- a/kmk/macros/rotary_encoder.py +++ /dev/null @@ -1,76 +0,0 @@ -import math - -from kmk.event_defs import (hid_report_event, keycode_down_event, - keycode_up_event) -from kmk.keycodes import Media -from kmk.rotary_encoder import RotaryEncoder - -VAL_FALSE = False + 1 -VAL_NONE = True + 2 -VAL_TRUE = True + 1 -VOL_UP_PRESS = keycode_down_event(Media.KC_AUDIO_VOL_UP) -VOL_UP_RELEASE = keycode_up_event(Media.KC_AUDIO_VOL_UP) -VOL_DOWN_PRESS = keycode_down_event(Media.KC_AUDIO_VOL_DOWN) -VOL_DOWN_RELEASE = keycode_up_event(Media.KC_AUDIO_VOL_DOWN) - - -class RotaryEncoderMacro: - def __init__(self, pos_pin, neg_pin, slop_history=1, slop_threshold=1): - self.encoder = RotaryEncoder(pos_pin, neg_pin) - self.max_history = slop_history - self.history = bytearray(slop_history) - self.history_idx = 0 - self.history_threshold = math.floor(slop_threshold * slop_history) - - def scan(self): - # Anti-slop logic - self.history[self.history_idx] = 0 - - reading = self.encoder.direction() - self.history[self.history_idx] = VAL_NONE if reading is None else reading + 1 - - self.history_idx += 1 - - if self.history_idx >= self.max_history: - self.history_idx = 0 - - nones = 0 - trues = 0 - falses = 0 - - for val in self.history: - if val == VAL_NONE: - nones += 1 - elif val == VAL_TRUE: - trues += 1 - elif val == VAL_FALSE: - falses += 1 - - if nones >= self.history_threshold: - return None - - if trues >= self.history_threshold: - return self.on_increase() - - if falses >= self.history_threshold: - return self.on_decrease() - - def on_decrease(self): - pass - - def on_increase(self): - pass - - -class VolumeRotaryEncoder(RotaryEncoderMacro): - def on_decrease(self): - yield VOL_DOWN_PRESS - yield hid_report_event - yield VOL_DOWN_RELEASE - yield hid_report_event - - def on_increase(self): - yield VOL_UP_PRESS - yield hid_report_event - yield VOL_UP_RELEASE - yield hid_report_event diff --git a/kmk/macros/simple.py b/kmk/macros/simple.py deleted file mode 100644 index 2bbf2d0..0000000 --- a/kmk/macros/simple.py +++ /dev/null @@ -1,28 +0,0 @@ -from kmk.keycodes import Keycodes, Macro, char_lookup, lookup_kc_with_cache -from kmk.string import ascii_letters, digits - - -def simple_key_sequence(seq): - def _simple_key_sequence(state): - return seq - - return Macro(keydown=_simple_key_sequence) - - -def send_string(message): - seq = [] - - for char in message: - kc = None - - if char in char_lookup: - kc = char_lookup[char] - elif char in ascii_letters + digits: - kc = lookup_kc_with_cache(char) - - if char.isupper(): - kc = Keycodes.Modifiers.KC_LSHIFT(kc) - - seq.append(kc) - - return simple_key_sequence(seq) diff --git a/kmk/macros/unicode.py b/kmk/macros/unicode.py index 84507ce..f95f014 100644 --- a/kmk/macros/unicode.py +++ b/kmk/macros/unicode.py @@ -1,16 +1,15 @@ from kmk.consts import UnicodeMode -from kmk.keycodes import (Common, Macro, Modifiers, - generate_codepoint_keysym_seq) +from kmk.keycodes import ALL_KEYS, KC, Macro from kmk.macros.simple import simple_key_sequence from kmk.types import AttrDict from kmk.util import get_wide_ordinal -IBUS_KEY_COMBO = Modifiers.KC_LCTRL(Modifiers.KC_LSHIFT(Common.KC_U)) -RALT_KEY = Modifiers.KC_RALT -U_KEY = Common.KC_U -ENTER_KEY = Common.KC_ENTER -RALT_DOWN_NO_RELEASE = Modifiers.KC_RALT(no_release=True) -RALT_UP_NO_PRESS = Modifiers.KC_RALT(no_press=True) +IBUS_KEY_COMBO = KC.LCTRL(KC.LSHIFT(KC.U)) +RALT_KEY = KC.RALT +U_KEY = KC.U +ENTER_KEY = KC.ENTER +RALT_DOWN_NO_RELEASE = KC.RALT(no_release=True) +RALT_UP_NO_PRESS = KC.RALT(no_press=True) def compile_unicode_string_sequences(string_table): @@ -31,6 +30,26 @@ def unicode_string_sequence(unistring): ]) +def generate_codepoint_keysym_seq(codepoint, expected_length=4): + # To make MacOS and Windows happy, always try to send + # sequences that are of length 4 at a minimum + # On Linux systems, we can happily send longer strings. + # They will almost certainly break on MacOS and Windows, + # but this is a documentation problem more than anything. + # Not sure how to send emojis on Mac/Windows like that, + # though, since (for example) the Canadian flag is assembled + # from two five-character codepoints, 1f1e8 and 1f1e6 + # + # As a bonus, this function can be pretty useful for + # leader dictionary keys as strings. + seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))] + + for idx, codepoint_fragment in enumerate(reversed(codepoint)): + seq[-(idx + 1)] = ALL_KEYS.get(codepoint_fragment) + + return seq + + def unicode_codepoint_sequence(codepoints): kc_seqs = ( generate_codepoint_keysym_seq(codepoint) diff --git a/kmk/rotary_encoder.py b/kmk/rotary_encoder.py deleted file mode 100644 index 96fe278..0000000 --- a/kmk/rotary_encoder.py +++ /dev/null @@ -1,57 +0,0 @@ -from kmk.pins import PULL_UP - - -class RotaryEncoder: - # Please don't ask. I don't know. All I know is bit_value - # works as expected. Here be dragons, etc. etc. - MIN_VALUE = False + 1 << 1 | True + 1 - MAX_VALUE = True + 1 << 1 | True + 1 - - def __init__(self, pos_pin, neg_pin): - self.pos_pin = pos_pin - self.neg_pin = neg_pin - - self.pos_pin.switch_to_input(pull=PULL_UP) - self.neg_pin.switch_to_input(pull=PULL_UP) - - self.prev_bit_value = self.bit_value() - - def value(self): - return (self.pos_pin.value(), self.neg_pin.value()) - - def bit_value(self): - ''' - Returns 2, 3, 5, or 6 based on the state of the rotary encoder's two - bits. This is a total hack but it does what we need pretty efficiently. - Shrug. - ''' - return self.pos_pin.value() + 1 << 1 | self.neg_pin.value() + 1 - - def direction(self): - ''' - Compares the current rotary position against the last seen position. - - Returns True if we're rotating "positively", False if we're rotating "negatively", - and None if no change could safely be detected for any reason (usually this - means the encoder itself did not change) - ''' - new_value = self.bit_value() - rolling_under = self.prev_bit_value == self.MIN_VALUE and new_value == self.MAX_VALUE - rolling_over = self.prev_bit_value == self.MAX_VALUE and new_value == self.MIN_VALUE - increasing = new_value > self.prev_bit_value - decreasing = new_value < self.prev_bit_value - self.prev_bit_value = new_value - - if rolling_over: - return True - elif rolling_under: - return False - - if increasing: - return True - if decreasing: - return False - - # Either no change, or not a type of change we can safely detect, - # so safely do nothing - return None diff --git a/kmk/types.py b/kmk/types.py index aeacb85..123f806 100644 --- a/kmk/types.py +++ b/kmk/types.py @@ -24,3 +24,29 @@ class Anything: class Passthrough: def __getattr__(self, attr): return Anything(attr) + + +class LayerKeyMeta: + def __init__(self, layer, kc=None): + self.layer = layer + self.kc = kc + + +class KeySequenceMeta: + def __init__(self, seq): + self.seq = seq + + +class KeySeqSleepMeta: + def __init__(self, ms): + self.ms = ms + + +class UnicodeModeKeyMeta: + def __init__(self, mode): + self.mode = mode + + +class TapDanceKeyMeta: + def __init__(self, codes): + self.codes = codes