kmk_firmware/kmk/keys.py
2022-07-22 09:21:30 -07:00

748 lines
23 KiB
Python

from micropython import const
import kmk.handlers.stock as handlers
from kmk.consts import UnicodeMode
from kmk.key_validators import key_seq_sleep_validator, unicode_mode_key_validator
from kmk.types import UnicodeModeKeyMeta
from kmk.utils import Debug
FIRST_KMK_INTERNAL_KEY = const(1000)
NEXT_AVAILABLE_KEY = 1000
KEY_SIMPLE = const(0)
KEY_MODIFIER = const(1)
KEY_CONSUMER = const(2)
ALL_ALPHAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALL_NUMBERS = '1234567890'
# since KC.1 isn't valid Python, alias to KC.N1
ALL_NUMBER_ALIASES = tuple(f'N{x}' for x in ALL_NUMBERS)
debug = Debug(__name__)
def maybe_make_key(code, names, *args, **kwargs):
def closure(candidate):
if candidate in names:
return make_key(code=code, names=names, *args, **kwargs)
return closure
def maybe_make_argumented_key(
validator=lambda *validator_args, **validator_kwargs: object(),
names=tuple(), # NOQA
*constructor_args,
**constructor_kwargs,
):
def closure(candidate):
if candidate in names:
return make_argumented_key(
validator, names, *constructor_args, **constructor_kwargs
)
return closure
def maybe_make_no_key(candidate):
# 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
keys = (
('NO', 'XXXXXXX'),
('TRANSPARENT', 'TRNS'),
)
for names in keys:
if candidate in names:
return make_key(
names=names,
on_press=handlers.passthrough,
on_release=handlers.passthrough,
)
def maybe_make_alpha_key(candidate):
if len(candidate) != 1:
return
candidate_upper = candidate.upper()
if candidate_upper in ALL_ALPHAS:
return make_key(
code=4 + ALL_ALPHAS.index(candidate_upper),
names=(candidate_upper, candidate.lower()),
)
def maybe_make_numeric_key(candidate):
if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES:
try:
offset = ALL_NUMBERS.index(candidate)
except ValueError:
offset = ALL_NUMBER_ALIASES.index(candidate)
return make_key(
code=30 + offset,
names=(ALL_NUMBERS[offset], ALL_NUMBER_ALIASES[offset]),
)
def maybe_make_mod_key(candidate):
# MEH = LCTL | LALT | LSFT
# HYPR = LCTL | LALT | LSFT | LGUI
mods = (
(0x01, ('LEFT_CONTROL', 'LCTRL', 'LCTL')),
(0x02, ('LEFT_SHIFT', 'LSHIFT', 'LSFT')),
(0x04, ('LEFT_ALT', 'LALT', 'LOPT')),
(0x08, ('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')),
(0x10, ('RIGHT_CONTROL', 'RCTRL', 'RCTL')),
(0x20, ('RIGHT_SHIFT', 'RSHIFT', 'RSFT')),
(0x40, ('RIGHT_ALT', 'RALT', 'ROPT')),
(0x80, ('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')),
(0x07, ('MEH',)),
(0x0F, ('HYPER', 'HYPR')),
)
for code, names in mods:
if candidate in names:
return make_key(code=code, names=names, type=KEY_MODIFIER)
def maybe_make_more_ascii(candidate):
codes = (
(40, ('ENTER', 'ENT', '\n')),
(41, ('ESCAPE', 'ESC')),
(42, ('BACKSPACE', 'BSPACE', 'BSPC', 'BKSP')),
(43, ('TAB', '\t')),
(44, ('SPACE', 'SPC', ' ')),
(45, ('MINUS', 'MINS', '-')),
(46, ('EQUAL', 'EQL', '=')),
(47, ('LBRACKET', 'LBRC', '[')),
(48, ('RBRACKET', 'RBRC', ']')),
(49, ('BACKSLASH', 'BSLASH', 'BSLS', '\\')),
(51, ('SEMICOLON', 'SCOLON', 'SCLN', ';')),
(52, ('QUOTE', 'QUOT', "'")),
(53, ('GRAVE', 'GRV', 'ZKHK', '`')),
(54, ('COMMA', 'COMM', ',')),
(55, ('DOT', '.')),
(56, ('SLASH', 'SLSH', '/')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_fn_key(candidate):
codes = (
(58, ('F1',)),
(59, ('F2',)),
(60, ('F3',)),
(61, ('F4',)),
(62, ('F5',)),
(63, ('F6',)),
(64, ('F7',)),
(65, ('F8',)),
(66, ('F9',)),
(67, ('F10',)),
(68, ('F11',)),
(69, ('F12',)),
(104, ('F13',)),
(105, ('F14',)),
(106, ('F15',)),
(107, ('F16',)),
(108, ('F17',)),
(109, ('F18',)),
(110, ('F19',)),
(111, ('F20',)),
(112, ('F21',)),
(113, ('F22',)),
(114, ('F23',)),
(115, ('F24',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_navlock_key(candidate):
codes = (
(57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')),
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# (130, ('LOCKING_CAPS', 'LCAP')),
(70, ('PRINT_SCREEN', 'PSCREEN', 'PSCR')),
(71, ('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')),
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# (132, ('LOCKING_SCROLL', 'LSCRL')),
(72, ('PAUSE', 'PAUS', 'BRK')),
(73, ('INSERT', 'INS')),
(74, ('HOME',)),
(75, ('PGUP',)),
(76, ('DELETE', 'DEL')),
(77, ('END',)),
(78, ('PGDOWN', 'PGDN')),
(79, ('RIGHT', 'RGHT')),
(80, ('LEFT',)),
(81, ('DOWN',)),
(82, ('UP',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_numpad_key(candidate):
codes = (
(83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
(84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')),
(85, ('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')),
(86, ('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')),
(87, ('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')),
(88, ('KP_ENTER', 'NUMPAD_ENTER', 'PENT')),
(89, ('KP_1', 'P1', 'NUMPAD_1')),
(90, ('KP_2', 'P2', 'NUMPAD_2')),
(91, ('KP_3', 'P3', 'NUMPAD_3')),
(92, ('KP_4', 'P4', 'NUMPAD_4')),
(93, ('KP_5', 'P5', 'NUMPAD_5')),
(94, ('KP_6', 'P6', 'NUMPAD_6')),
(95, ('KP_7', 'P7', 'NUMPAD_7')),
(96, ('KP_8', 'P8', 'NUMPAD_8')),
(97, ('KP_9', 'P9', 'NUMPAD_9')),
(98, ('KP_0', 'P0', 'NUMPAD_0')),
(99, ('KP_DOT', 'PDOT', 'NUMPAD_DOT')),
(103, ('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')),
(133, ('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')),
(134, ('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_shifted_key(candidate):
codes = (
(30, ('EXCLAIM', 'EXLM', '!')),
(31, ('AT', '@')),
(32, ('HASH', 'POUND', '#')),
(33, ('DOLLAR', 'DLR', '$')),
(34, ('PERCENT', 'PERC', '%')),
(35, ('CIRCUMFLEX', 'CIRC', '^')),
(36, ('AMPERSAND', 'AMPR', '&')),
(37, ('ASTERISK', 'ASTR', '*')),
(38, ('LEFT_PAREN', 'LPRN', '(')),
(39, ('RIGHT_PAREN', 'RPRN', ')')),
(45, ('UNDERSCORE', 'UNDS', '_')),
(46, ('PLUS', '+')),
(47, ('LEFT_CURLY_BRACE', 'LCBR', '{')),
(48, ('RIGHT_CURLY_BRACE', 'RCBR', '}')),
(49, ('PIPE', '|')),
(51, ('COLON', 'COLN', ':')),
(52, ('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')),
(53, ('TILDE', 'TILD', '~')),
(54, ('LEFT_ANGLE_BRACKET', 'LABK', '<')),
(55, ('RIGHT_ANGLE_BRACKET', 'RABK', '>')),
(56, ('QUESTION', 'QUES', '?')),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names, has_modifiers={KC.LSFT.code})
def maybe_make_international_key(candidate):
codes = (
(50, ('NONUS_HASH', 'NUHS')),
(100, ('NONUS_BSLASH', 'NUBS')),
(101, ('APP', 'APPLICATION', 'SEL', 'WINMENU')),
(135, ('INT1', 'RO')),
(136, ('INT2', 'KANA')),
(137, ('INT3', 'JYEN')),
(138, ('INT4', 'HENK')),
(139, ('INT5', 'MHEN')),
(140, ('INT6',)),
(141, ('INT7',)),
(142, ('INT8',)),
(143, ('INT9',)),
(144, ('LANG1', 'HAEN')),
(145, ('LANG2', 'HAEJ')),
(146, ('LANG3',)),
(147, ('LANG4',)),
(148, ('LANG5',)),
(149, ('LANG6',)),
(150, ('LANG7',)),
(151, ('LANG8',)),
(152, ('LANG9',)),
)
for code, names in codes:
if candidate in names:
return make_key(code=code, names=names)
def maybe_make_unicode_key(candidate):
keys = (
(
('UC_MODE_NOOP', 'UC_DISABLE'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.NOOP),
),
(
('UC_MODE_LINUX', 'UC_MODE_IBUS'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.IBUS),
),
(
('UC_MODE_MACOS', 'UC_MODE_OSX', 'US_MODE_RALT'),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.RALT),
),
(
('UC_MODE_WINC',),
handlers.uc_mode_pressed,
UnicodeModeKeyMeta(UnicodeMode.WINC),
),
)
for names, handler, meta in keys:
if candidate in names:
return make_key(names=names, on_press=handler, meta=meta)
if candidate in ('UC_MODE',):
return make_argumented_key(
names=('UC_MODE',),
validator=unicode_mode_key_validator,
on_press=handlers.uc_mode_pressed,
)
def maybe_make_firmware_key(candidate):
keys = (
((('BLE_REFRESH',), handlers.ble_refresh)),
((('BOOTLOADER',), handlers.bootloader)),
((('DEBUG', 'DBG'), handlers.debug_pressed)),
((('HID_SWITCH', 'HID'), handlers.hid_switch)),
((('RELOAD', 'RLD'), handlers.reload)),
((('RESET',), handlers.reset)),
)
for names, handler in keys:
if candidate in names:
return make_key(names=names, on_press=handler)
KEY_GENERATORS = (
maybe_make_no_key,
maybe_make_alpha_key,
maybe_make_numeric_key,
maybe_make_firmware_key,
maybe_make_key(
None,
('BKDL',),
on_press=handlers.bkdl_pressed,
on_release=handlers.bkdl_released,
),
maybe_make_key(
None,
('GESC', 'GRAVE_ESC'),
on_press=handlers.gesc_pressed,
on_release=handlers.gesc_released,
),
# A dummy key to trigger a sleep_ms call in a sequence of other keys in a
# simple sequence macro.
maybe_make_argumented_key(
key_seq_sleep_validator,
('MACRO_SLEEP_MS', 'SLEEP_IN_SEQ'),
on_press=handlers.sleep_pressed,
),
maybe_make_mod_key,
# More ASCII standard keys
maybe_make_more_ascii,
# Function Keys
maybe_make_fn_key,
# Lock Keys, Navigation, etc.
maybe_make_navlock_key,
# Numpad
# FIXME: Investigate whether this key actually works, and
# uncomment when/if it does.
# maybe_make_key(131, ('LOCKING_NUM', 'LNUM')),
maybe_make_numpad_key,
# 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.
maybe_make_shifted_key,
# International
maybe_make_international_key,
maybe_make_unicode_key,
)
class KeyAttrDict:
__cache = {}
def __iter__(self):
return self.__cache.__iter__()
def __setitem__(self, key, value):
self.__cache.__setitem__(key, value)
def __getattr__(self, key):
return self.__getitem__(key)
def get(self, key, default=None):
try:
return self.__getitem__(key)
except Exception:
return default
def clear(self):
self.__cache.clear()
def __getitem__(self, key):
try:
return self.__cache[key]
except KeyError:
pass
for func in KEY_GENERATORS:
maybe_key = func(key)
if maybe_key:
break
else:
raise ValueError(f'Invalid key: {key}')
if debug.enabled:
debug(f'{key}: {maybe_key}')
return self.__cache[key]
# Global state, will be filled in throughout this file, and
# anywhere the user creates custom keys
KC = KeyAttrDict()
class Key:
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_release)
self._handle_press = on_press
self._handle_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
return type(self)(
code=self.code,
has_modifiers=self.has_modifiers,
no_press=no_press,
no_release=no_release,
on_press=self._handle_press,
on_release=self._handle_release,
meta=self.meta,
)
def __repr__(self):
return f'Key(code={self.code}, has_modifiers={self.has_modifiers})'
def on_press(self, state, coord_int=None, coord_raw=None):
if hasattr(self, '_pre_press_handlers'):
for fn in self._pre_press_handlers:
if not fn(self, state, KC, coord_int, coord_raw):
return None
ret = self._handle_press(self, state, KC, coord_int, coord_raw)
if hasattr(self, '_post_press_handlers'):
for fn in self._post_press_handlers:
fn(self, state, KC, coord_int, coord_raw)
return ret
def on_release(self, state, coord_int=None, coord_raw=None):
if hasattr(self, '_pre_release_handlers'):
for fn in self._pre_release_handlers:
if not fn(self, state, KC, coord_int, coord_raw):
return None
ret = self._handle_release(self, state, KC, coord_int, coord_raw)
if hasattr(self, '_post_release_handlers'):
for fn in self._post_release_handlers:
fn(self, state, KC, coord_int, coord_raw)
return ret
def clone(self):
'''
Return a shallow clone of the current key without any pre/post press/release
handlers attached. Almost exclusively useful for creating non-colliding keys
to use such handlers.
'''
return type(self)(
code=self.code,
has_modifiers=self.has_modifiers,
no_press=self.no_press,
no_release=self.no_release,
on_press=self._handle_press,
on_release=self._handle_release,
meta=self.meta,
)
def before_press_handler(self, fn):
'''
Attach a callback to be run prior to the on_press handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
If return value of the provided callback is evaluated to False, press
processing is cancelled. Exceptions are _not_ caught, and will likely
crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_pre_press_handlers'):
self._pre_press_handlers = []
self._pre_press_handlers.append(fn)
return self
def after_press_handler(self, fn):
'''
Attach a callback to be run after the on_release handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
The return value of the provided callback is discarded. Exceptions are _not_
caught, and will likely crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_post_press_handlers'):
self._post_press_handlers = []
self._post_press_handlers.append(fn)
return self
def before_release_handler(self, fn):
'''
Attach a callback to be run prior to the on_release handler for this
key. Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
If return value of the provided callback evaluates to False, the release
processing is cancelled. Exceptions are _not_ caught, and will likely crash
KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_pre_release_handlers'):
self._pre_release_handlers = []
self._pre_release_handlers.append(fn)
return self
def after_release_handler(self, fn):
'''
Attach a callback to be run after the on_release handler for this key.
Receives the following:
- self (this Key instance)
- state (the current InternalState)
- KC (the global KC lookup table, for convenience)
- coord_int (an internal integer representation of the matrix coordinate
for the pressed key - this is likely not useful to end users, but is
provided for consistency with the internal handlers)
- coord_raw (an X,Y tuple of the matrix coordinate - also likely not useful)
The return value of the provided callback is discarded. Exceptions are _not_
caught, and will likely crash KMK if not handled within your function.
These handlers are run in attachment order: handlers provided by earlier
calls of this method will be executed before those provided by later calls.
'''
if not hasattr(self, '_post_release_handlers'):
self._post_release_handlers = []
self._post_release_handlers.append(fn)
return self
class ModifierKey(Key):
FAKE_CODE = const(-1)
def __call__(self, modified_key=None, no_press=None, no_release=None):
if modified_key is None:
return super().__call__(no_press=no_press, no_release=no_release)
modifiers = set()
code = modified_key.code
if self.code != ModifierKey.FAKE_CODE:
modifiers.add(self.code)
if self.has_modifiers:
modifiers |= self.has_modifiers
if modified_key.has_modifiers:
modifiers |= modified_key.has_modifiers
if isinstance(modified_key, ModifierKey):
if modified_key.code != ModifierKey.FAKE_CODE:
modifiers.add(modified_key.code)
code = ModifierKey.FAKE_CODE
return type(modified_key)(
code=code,
has_modifiers=modifiers,
no_press=no_press,
no_release=no_release,
on_press=modified_key._handle_press,
on_release=modified_key._handle_release,
meta=modified_key.meta,
)
def __repr__(self):
return f'ModifierKey(code={self.code}, has_modifiers={self.has_modifiers})'
class ConsumerKey(Key):
pass
def make_key(code=None, names=tuple(), type=KEY_SIMPLE, **kwargs): # NOQA
'''
Create a new key, aliased by `names` in the KC lookup table.
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.
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.
Names are case sensitive.
All **kwargs are passed to the Key constructor
'''
global NEXT_AVAILABLE_KEY
if type == KEY_SIMPLE:
constructor = Key
elif type == KEY_MODIFIER:
constructor = ModifierKey
elif type == KEY_CONSUMER:
constructor = ConsumerKey
else:
raise ValueError('Unrecognized key type')
if code is None:
code = NEXT_AVAILABLE_KEY
NEXT_AVAILABLE_KEY += 1
elif code >= FIRST_KMK_INTERNAL_KEY:
# Try to ensure future auto-generated internal keycodes won't
# be overridden by continuing to +1 the sequence from the provided
# code
NEXT_AVAILABLE_KEY = max(NEXT_AVAILABLE_KEY, code + 1)
key = constructor(code=code, **kwargs)
for name in names:
KC[name] = key
return key
def make_mod_key(code, names, *args, **kwargs):
return make_key(code, names, *args, **kwargs, type=KEY_MODIFIER)
def make_shifted_key(code, names):
return make_key(code, names, has_modifiers={KC.LSFT.code})
def make_consumer_key(*args, **kwargs):
return make_key(*args, **kwargs, type=KEY_CONSUMER)
# 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(),
names=tuple(), # NOQA
*constructor_args,
**constructor_kwargs,
):
global NEXT_AVAILABLE_KEY
def _argumented_key(*user_args, **user_kwargs):
global NEXT_AVAILABLE_KEY
meta = validator(*user_args, **user_kwargs)
if meta:
key = Key(
NEXT_AVAILABLE_KEY, meta=meta, *constructor_args, **constructor_kwargs
)
NEXT_AVAILABLE_KEY += 1
return key
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.'
)
for name in names:
KC[name] = _argumented_key
return _argumented_key