~jacqueline/kmk-crkbd

16a10af91245d9700f0a2b20db5cc376c9cd291b — jacqueline 3 months ago main
hi robin
A  => boot.py +3 -0
@@ 1,3 @@
import supervisor

supervisor.set_next_stack_limit(4096 + 4096)

A  => kb.py +44 -0
@@ 1,44 @@
import board

from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.scanners import DiodeOrientation


class KMKKeyboard(_KMKKeyboard):
    col_pins = (
        board.A3,
        board.A2,
        board.A1,
        board.A0,
        board.SCK,
        board.MISO,
    )















    row_pins = (board.D4, board.D5, board.D6, board.D7)
    diode_orientation = DiodeOrientation.COLUMNS
    data_pin = board.D2
    data_pin2 = board.D3
    rgb_pixel_pin = board.D0
    #i2c = board.I2C

    # flake8: noqa
    coord_mapping = [
     0,  1,  2,  3,  4,  5,  29, 28, 27, 26, 25, 24,
     6,  7,  8,  9, 10, 11,  35, 34, 33, 32, 31, 30,
    12, 13, 14, 15, 16, 17,  41, 40, 39, 38, 37, 36,
                21, 22, 23,  47, 46, 45,
    ]

A  => kmk/__init__.py +0 -0
A  => kmk/consts.py +8 -0
@@ 1,8 @@
from micropython import const


class UnicodeMode:
    NOOP = const(0)
    LINUX = IBUS = const(1)
    MACOS = OSX = RALT = const(2)
    WINC = const(3)

A  => kmk/extensions/__init__.py +51 -0
@@ 1,51 @@
class InvalidExtensionEnvironment(Exception):
    pass


class Extension:
    _enabled = True

    def enable(self, keyboard):
        self._enabled = True

        self.on_runtime_enable(keyboard)

    def disable(self, keyboard):
        self._enabled = False

        self.on_runtime_disable(keyboard)

    # The below methods should be implemented by subclasses

    def on_runtime_enable(self, keyboard):
        raise NotImplementedError

    def on_runtime_disable(self, keyboard):
        raise NotImplementedError

    def during_bootup(self, keyboard):
        raise NotImplementedError

    def before_matrix_scan(self, keyboard):
        '''
        Return value will be injected as an extra matrix update
        '''
        raise NotImplementedError

    def after_matrix_scan(self, keyboard):
        '''
        Return value will be replace matrix update if supplied
        '''
        raise NotImplementedError

    def before_hid_send(self, keyboard):
        raise NotImplementedError

    def after_hid_send(self, keyboard):
        raise NotImplementedError

    def on_powersave_enable(self, keyboard):
        raise NotImplementedError

    def on_powersave_disable(self, keyboard):
        raise NotImplementedError

A  => kmk/extensions/international.py +59 -0
@@ 1,59 @@
'''Adds international keys'''
from kmk.extensions import Extension
from kmk.keys import make_key


class International(Extension):
    '''Adds international keys'''

    def __init__(self):
        # International
        make_key(code=50, names=('NONUS_HASH', 'NUHS'))
        make_key(code=100, names=('NONUS_BSLASH', 'NUBS'))
        make_key(code=101, names=('APP', 'APPLICATION', 'SEL', 'WINMENU'))

        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',))

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        return

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

A  => kmk/extensions/keymap_extras/keymap_jp.py +34 -0
@@ 1,34 @@
# What's this?
# This is a keycode conversion script. With this, KMK will work as a JIS keyboard.

# Usage
# ```python
# import kmk.extensions.keymap_extras.keymap_jp
# ```

from kmk.keys import KC

KC.CIRC = KC.EQL  # ^
KC.AT = KC.LBRC  # @
KC.LBRC = KC.RBRC  # [
KC.EISU = KC.CAPS  # Eisū (英数)
KC.COLN = KC.QUOT  # :
KC.LCBR = KC.LSFT(KC.RBRC)  # {
KC.RBRC = KC.NUHS  # ]
KC.BSLS = KC.INT1  # (backslash)
KC.PLUS = KC.LSFT(KC.SCLN)
KC.TILD = KC.LSFT(KC.EQL)  # ~
KC.GRV = KC.LSFT(KC.AT)  # `
KC.DQUO = KC.LSFT(KC.N2)  # "
KC.AMPR = KC.LSFT(KC.N6)  # &
KC.ASTR = KC.LSFT(KC.QUOT)  # *
KC.QUOT = KC.LSFT(KC.N7)  # '
KC.LPRN = KC.LSFT(KC.N8)  # (
KC.RPRN = KC.LSFT(KC.N9)  # )
KC.EQL = KC.LSFT(KC.MINS)  # =
KC.PIPE = KC.LSFT(KC.INT3)  # |
KC.RCBR = KC.LSFT(KC.NUHS)  # }
KC.LABK = KC.LSFT(KC.COMM)  # <
KC.RABK = KC.LSFT(KC.DOT)  # >
KC.QUES = KC.LSFT(KC.SLSH)  # ?
KC.UNDS = KC.LSFT(KC.INT1)  # _

A  => kmk/extensions/led.py +257 -0
@@ 1,257 @@
import pwmio
from math import e, exp, pi, sin

from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_argumented_key, make_key
from kmk.utils import clamp


class LEDKeyMeta:
    def __init__(self, *leds):
        self.leds = leds
        self.brightness = None


class AnimationModes:
    OFF = 0
    STATIC = 1
    STATIC_STANDBY = 2
    BREATHING = 3
    USER = 4


class LED(Extension):
    def __init__(
        self,
        led_pin,
        brightness=50,
        brightness_step=5,
        brightness_limit=100,
        breathe_center=1.5,
        animation_mode=AnimationModes.STATIC,
        animation_speed=1,
        user_animation=None,
        val=100,
    ):
        try:
            pins_iter = iter(led_pin)
        except TypeError:
            pins_iter = [led_pin]

        try:
            self._leds = [pwmio.PWMOut(pin) for pin in pins_iter]
        except Exception as e:
            print(e)
            raise InvalidExtensionEnvironment(
                'Unable to create pwmio.PWMOut() instance with provided led_pin'
            )

        self._brightness = brightness
        self._pos = 0
        self._effect_init = False
        self._enabled = True

        self.brightness_step = brightness_step
        self.brightness_limit = brightness_limit
        self.animation_mode = animation_mode
        self.animation_speed = animation_speed
        self.breathe_center = breathe_center
        self.val = val

        if user_animation is not None:
            self.user_animation = user_animation

        make_argumented_key(
            names=('LED_TOG',),
            validator=self._led_key_validator,
            on_press=self._key_led_tog,
        )
        make_argumented_key(
            names=('LED_INC',),
            validator=self._led_key_validator,
            on_press=self._key_led_inc,
        )
        make_argumented_key(
            names=('LED_DEC',),
            validator=self._led_key_validator,
            on_press=self._key_led_dec,
        )
        make_argumented_key(
            names=('LED_SET',),
            validator=self._led_set_key_validator,
            on_press=self._key_led_set,
        )
        make_key(names=('LED_ANI',), on_press=self._key_led_ani)
        make_key(names=('LED_AND',), on_press=self._key_led_and)
        make_key(
            names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
        )
        make_key(
            names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
        )

    def __repr__(self):
        return f'LED({self._to_dict()})'

    def _to_dict(self):
        return {
            '_brightness': self._brightness,
            '_pos': self._pos,
            'brightness_step': self.brightness_step,
            'brightness_limit': self.brightness_limit,
            'animation_mode': self.animation_mode,
            'animation_speed': self.animation_speed,
            'breathe_center': self.breathe_center,
            'val': self.val,
        }

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        if self._enabled and self.animation_mode:
            self.animate()
        return

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

    def _init_effect(self):
        self._pos = 0
        self._effect_init = False
        return self

    def set_brightness(self, percent, leds=None):
        leds = leds or range(0, len(self._leds))
        for i in leds:
            self._leds[i].duty_cycle = int(percent / 100 * 65535)

    def step_brightness(self, step, leds=None):
        leds = leds or range(0, len(self._leds))
        for i in leds:
            brightness = int(self._leds[i].duty_cycle / 65535 * 100) + step
            self.set_brightness(clamp(brightness), [i])

    def increase_brightness(self, step=None, leds=None):
        if step is None:
            step = self.brightness_step
        self.step_brightness(step, leds)

    def decrease_brightness(self, step=None, leds=None):
        if step is None:
            step = self.brightness_step
        self.step_brightness(-step, leds)

    def off(self):
        self.set_brightness(0)

    def increase_ani(self):
        '''
        Increases animation speed by 1 amount stopping at 10
        :param step:
        '''
        if (self.animation_speed + 1) >= 10:
            self.animation_speed = 10
        else:
            self.val += 1

    def decrease_ani(self):
        '''
        Decreases animation speed by 1 amount stopping at 0
        :param step:
        '''
        if (self.val - 1) <= 0:
            self.val = 0
        else:
            self.val -= 1

    def effect_breathing(self):
        # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
        # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
        sined = sin((self._pos / 255.0) * pi)
        multip_1 = exp(sined) - self.breathe_center / e
        multip_2 = self.brightness_limit / (e - 1 / e)

        self._brightness = int(multip_1 * multip_2)
        self._pos = (self._pos + self.animation_speed) % 256
        self.set_brightness(self._brightness)

    def effect_static(self):
        self.set_brightness(self._brightness)
        # Set animation mode to none to prevent cycles from being wasted
        self.animation_mode = None

    def animate(self):
        '''
        Activates a "step" in the animation based on the active mode
        :return: Returns the new state in animation
        '''
        if self._effect_init:
            self._init_effect()
        if self._enabled:
            if self.animation_mode == AnimationModes.BREATHING:
                return self.effect_breathing()
            elif self.animation_mode == AnimationModes.STATIC:
                return self.effect_static()
            elif self.animation_mode == AnimationModes.USER:
                return self.user_animation(self)
        else:
            self.off()

    def _led_key_validator(self, *leds):
        if leds:
            for led in leds:
                assert self._leds[led]
        return LEDKeyMeta(*leds)

    def _led_set_key_validator(self, brightness, *leds):
        meta = self._led_key_validator(*leds)
        meta.brightness = brightness
        return meta

    def _key_led_tog(self, *args, **kwargs):
        if self.animation_mode == AnimationModes.STATIC_STANDBY:
            self.animation_mode = AnimationModes.STATIC

        self._enabled = not self._enabled

    def _key_led_inc(self, key, *args, **kwargs):
        self.increase_brightness(leds=key.meta.leds)

    def _key_led_dec(self, key, *args, **kwargs):
        self.decrease_brightness(leds=key.meta.leds)

    def _key_led_set(self, key, *args, **kwargs):
        self.set_brightness(percent=key.meta.brightness, leds=key.meta.leds)

    def _key_led_ani(self, *args, **kwargs):
        self.increase_ani()

    def _key_led_and(self, *args, **kwargs):
        self.decrease_ani()

    def _key_led_mode_static(self, *args, **kwargs):
        self._effect_init = True
        self.animation_mode = AnimationModes.STATIC

    def _key_led_mode_breathe(self, *args, **kwargs):
        self._effect_init = True
        self.animation_mode = AnimationModes.BREATHING

A  => kmk/extensions/lock_status.py +85 -0
@@ 1,85 @@
import usb_hid

from kmk.extensions import Extension
from kmk.hid import HIDUsage


class LockCode:
    NUMLOCK = 0x01
    CAPSLOCK = 0x02
    SCROLLLOCK = 0x04
    COMPOSE = 0x08
    KANA = 0x10
    RESERVED = 0x20


class LockStatus(Extension):
    def __init__(self):
        self.report = None
        self.hid = None
        self._report_updated = False
        for device in usb_hid.devices:
            if device.usage == HIDUsage.KEYBOARD:
                self.hid = device

    def __repr__(self):
        return f'LockStatus(report={self.report})'

    def during_bootup(self, sandbox):
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        if self.hid:
            report = self.hid.get_last_received_report()
            if report and report[0] != self.report:
                self.report = report[0]
                self._report_updated = True
            else:
                self._report_updated = False
        else:
            # _report_updated shouldn't ever be True if hid is
            # falsy, but I would rather be safe than sorry.
            self._report_updated = False
        return

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

    @property
    def report_updated(self):
        return self._report_updated

    def check_state(self, lock_code):
        # This is false if there's no valid report, or all report bits are zero
        if self.report:
            return bool(self.report & lock_code)
        else:
            # Just in case, default to False if we don't know anything
            return False

    def get_num_lock(self):
        return self.check_state(LockCode.NUMLOCK)

    def get_caps_lock(self):
        return self.check_state(LockCode.CAPSLOCK)

    def get_scroll_lock(self):
        return self.check_state(LockCode.SCROLLLOCK)

    def get_compose(self):
        return self.check_state(LockCode.COMPOSE)

    def get_kana(self):
        return self.check_state(LockCode.KANA)

A  => kmk/extensions/media_keys.py +57 -0
@@ 1,57 @@
from kmk.extensions import Extension
from kmk.keys import make_consumer_key


class MediaKeys(Extension):
    def __init__(self):
        # 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=111, names=('BRIGHTNESS_UP', 'BRIU'))  # 0x6F
        make_consumer_key(code=112, names=('BRIGHTNESS_DOWN', 'BRID'))  # 0x70
        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

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        return

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

A  => kmk/extensions/peg_oled_display.py +161 -0
@@ 1,161 @@
import busio
import gc

import adafruit_displayio_ssd1306
import displayio
import terminalio
from adafruit_display_text import label

from kmk.extensions import Extension


class OledDisplayMode:
    TXT = 0
    IMG = 1


class OledReactionType:
    STATIC = 0
    LAYER = 1


class OledData:
    def __init__(
        self,
        image=None,
        corner_one=None,
        corner_two=None,
        corner_three=None,
        corner_four=None,
    ):
        if image:
            self.data = [image]
        elif corner_one and corner_two and corner_three and corner_four:
            self.data = [corner_one, corner_two, corner_three, corner_four]


class Oled(Extension):
    def __init__(
        self,
        views,
        toDisplay=OledDisplayMode.TXT,
        oWidth=128,
        oHeight=32,
        flip: bool = False,
    ):
        displayio.release_displays()
        self.rotation = 180 if flip else 0
        self._views = views.data
        self._toDisplay = toDisplay
        self._width = oWidth
        self._height = oHeight
        self._prevLayers = 0
        gc.collect()

    def returnCurrectRenderText(self, layer, singleView):
        # for now we only have static things and react to layers. But when we react to battery % and wpm we can handle the logic here
        if singleView[0] == OledReactionType.STATIC:
            return singleView[1][0]
        if singleView[0] == OledReactionType.LAYER:
            return singleView[1][layer]

    def renderOledTextLayer(self, layer):
        splash = displayio.Group()
        splash.append(
            label.Label(
                terminalio.FONT,
                text=self.returnCurrectRenderText(layer, self._views[0]),
                color=0xFFFFFF,
                x=0,
                y=10,
            )
        )
        splash.append(
            label.Label(
                terminalio.FONT,
                text=self.returnCurrectRenderText(layer, self._views[1]),
                color=0xFFFFFF,
                x=64,
                y=10,
            )
        )
        splash.append(
            label.Label(
                terminalio.FONT,
                text=self.returnCurrectRenderText(layer, self._views[2]),
                color=0xFFFFFF,
                x=0,
                y=25,
            )
        )
        splash.append(
            label.Label(
                terminalio.FONT,
                text=self.returnCurrectRenderText(layer, self._views[3]),
                color=0xFFFFFF,
                x=64,
                y=25,
            )
        )
        self._display.show(splash)
        gc.collect()

    def renderOledImgLayer(self, layer):
        splash = displayio.Group()
        odb = displayio.OnDiskBitmap(
            '/' + self.returnCurrectRenderText(layer, self._views[0])
        )
        image = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)
        splash.append(image)
        self._display.show(splash)
        gc.collect()

    def updateOLED(self, sandbox):
        if self._toDisplay == OledDisplayMode.TXT:
            self.renderOledTextLayer(sandbox.active_layers[0])
        if self._toDisplay == OledDisplayMode.IMG:
            self.renderOledImgLayer(sandbox.active_layers[0])
        gc.collect()

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, board):
        displayio.release_displays()
        i2c = busio.I2C(board.SCL, board.SDA)
        self._display = adafruit_displayio_ssd1306.SSD1306(
            displayio.I2CDisplay(i2c, device_address=0x3C),
            width=self._width,
            height=self._height,
            rotation=self.rotation,
        )
        if self._toDisplay == OledDisplayMode.TXT:
            self.renderOledTextLayer(0)
        if self._toDisplay == OledDisplayMode.IMG:
            self.renderOledImgLayer(0)
        return

    def before_matrix_scan(self, sandbox):
        if sandbox.active_layers[0] != self._prevLayers:
            self._prevLayers = sandbox.active_layers[0]
            self.updateOLED(sandbox)
        return

    def after_matrix_scan(self, sandbox):

        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        return

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

A  => kmk/extensions/peg_rgb_matrix.py +201 -0
@@ 1,201 @@
import neopixel

from storage import getmount

from kmk.extensions import Extension
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key


class Color:
    OFF = [0, 0, 0]
    BLACK = OFF
    WHITE = [249, 249, 249]
    RED = [255, 0, 0]
    AZURE = [153, 245, 255]
    BLUE = [0, 0, 255]
    CYAN = [0, 255, 255]
    GREEN = [0, 255, 0]
    YELLOW = [255, 247, 0]
    MAGENTA = [255, 0, 255]
    ORANGE = [255, 77, 0]
    PURPLE = [255, 0, 242]
    TEAL = [0, 128, 128]
    PINK = [255, 0, 255]


class Rgb_matrix_data:
    def __init__(self, keys=[], underglow=[]):
        if len(keys) == 0:
            print('No colors passed for your keys')
            return
        if len(underglow) == 0:
            print('No colors passed for your underglow')
            return
        self.data = keys + underglow

    @staticmethod
    def generate_led_map(
        number_of_keys, number_of_underglow, key_color, underglow_color
    ):
        keys = [key_color] * number_of_keys
        underglow = [underglow_color] * number_of_underglow
        print(f'Rgb_matrix_data(keys={keys},\nunderglow={underglow})')


class Rgb_matrix(Extension):
    def __init__(
        self,
        rgb_order=(1, 0, 2),  # GRB WS2812
        disable_auto_write=False,
        ledDisplay=[],
        split=False,
        rightSide=False,
    ):
        name = str(getmount('/').label)
        self.rgb_order = rgb_order
        self.disable_auto_write = disable_auto_write
        self.split = split
        self.rightSide = rightSide
        self.brightness_step = 0.1
        self.brightness = 0

        if name.endswith('L'):
            self.rightSide = False
        elif name.endswith('R'):
            self.rightSide = True
        if type(ledDisplay) is Rgb_matrix_data:
            self.ledDisplay = ledDisplay.data
        else:
            self.ledDisplay = ledDisplay

        make_key(
            names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_BRI',), on_press=self._rgb_bri, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_BRD',), on_press=self._rgb_brd, on_release=handler_passthrough
        )

    def _rgb_tog(self, *args, **kwargs):
        if self.enable:
            self.off()
        else:
            self.on()
        self.enable = not self.enable

    def _rgb_bri(self, *args, **kwargs):
        self.increase_brightness()

    def _rgb_brd(self, *args, **kwargs):
        self.decrease_brightness()

    def on(self):
        if self.neopixel:
            self.setBasedOffDisplay()
            self.neopixel.show()

    def off(self):
        if self.neopixel:
            self.set_rgb_fill((0, 0, 0))

    def set_rgb_fill(self, rgb):
        if self.neopixel:
            self.neopixel.fill(rgb)
            if self.disable_auto_write:
                self.neopixel.show()

    def set_brightness(self, brightness=None):
        if brightness is None:
            brightness = self.brightness

        if self.neopixel:
            self.neopixel.brightness = brightness
            if self.disable_auto_write:
                self.neopixel.show()

    def increase_brightness(self, step=None):
        if step is None:
            step = self.brightness_step

        self.brightness = (
            self.brightness + step if self.brightness + step <= 1.0 else 1.0
        )

        self.set_brightness(self.brightness)

    def decrease_brightness(self, step=None):
        if step is None:
            step = self.brightness_step

        self.brightness = (
            self.brightness - step if self.brightness - step >= 0.0 else 0.0
        )
        self.set_brightness(self.brightness)

    def setBasedOffDisplay(self):
        if self.split:
            for i, val in enumerate(self.ledDisplay):
                if self.rightSide:
                    if self.keyPos[i] >= (self.num_pixels / 2):
                        self.neopixel[int(self.keyPos[i] - (self.num_pixels / 2))] = (
                            val[0],
                            val[1],
                            val[2],
                        )
                else:
                    if self.keyPos[i] <= (self.num_pixels / 2):
                        self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
        else:
            for i, val in enumerate(self.ledDisplay):
                self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, board):
        self.neopixel = neopixel.NeoPixel(
            board.rgb_pixel_pin,
            board.num_pixels,
            brightness=board.brightness_limit,
            pixel_order=self.rgb_order,
            auto_write=not self.disable_auto_write,
        )
        self.num_pixels = board.num_pixels
        self.keyPos = board.led_key_pos
        self.brightness = board.brightness_limit
        self.on()
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        return

    def on_powersave_enable(self, sandbox):
        if self.neopixel:
            self.neopixel.brightness = (
                self.neopixel.brightness / 2
                if self.neopixel.brightness / 2 > 0
                else 0.1
            )
            if self.disable_auto_write:
                self.neopixel.show()

    def on_powersave_disable(self, sandbox):
        if self.neopixel:
            self.neopixel.brightness = self.brightness
            if self.disable_auto_write:
                self.neopixel.show()

A  => kmk/extensions/rgb.py +593 -0
@@ 1,593 @@
from adafruit_pixelbuf import PixelBuf
from math import e, exp, pi, sin

from kmk.extensions import Extension
from kmk.handlers.stock import passthrough as handler_passthrough
from kmk.keys import make_key
from kmk.kmktime import PeriodicTimer
from kmk.utils import Debug, clamp

debug = Debug(__name__)

rgb_config = {}


def hsv_to_rgb(hue, sat, val):
    '''
    Converts HSV values, and returns a tuple of RGB values
    :param hue:
    :param sat:
    :param val:
    :return: (r, g, b)
    '''
    if sat == 0:
        return (val, val, val)

    hue = 6 * (hue & 0xFF)
    frac = hue & 0xFF
    sxt = hue >> 8

    base = (0xFF - sat) * val
    color = (val * sat * frac) >> 8
    val <<= 8

    if sxt == 0:
        r = val
        g = base + color
        b = base
    elif sxt == 1:
        r = val - color
        g = val
        b = base
    elif sxt == 2:
        r = base
        g = val
        b = base + color
    elif sxt == 3:
        r = base
        g = val - color
        b = val
    elif sxt == 4:
        r = base + color
        g = base
        b = val
    elif sxt == 5:
        r = val
        g = base
        b = val - color

    return (r >> 8), (g >> 8), (b >> 8)


def hsv_to_rgbw(self, hue, sat, val):
    '''
    Converts HSV values, and returns a tuple of RGBW values
    :param hue:
    :param sat:
    :param val:
    :return: (r, g, b, w)
    '''
    rgb = hsv_to_rgb(hue, sat, val)
    return rgb[0], rgb[1], rgb[2], min(rgb)


class AnimationModes:
    OFF = 0
    STATIC = 1
    STATIC_STANDBY = 2
    BREATHING = 3
    RAINBOW = 4
    BREATHING_RAINBOW = 5
    KNIGHT = 6
    SWIRL = 7
    USER = 8


class RGB(Extension):
    pos = 0

    def __init__(
        self,
        pixel_pin,
        num_pixels=0,
        val_limit=255,
        hue_default=0,
        sat_default=255,
        rgb_order=(1, 0, 2),  # GRB WS2812
        val_default=255,
        hue_step=4,
        sat_step=13,
        val_step=13,
        animation_speed=1,
        breathe_center=1,  # 1.0-2.7
        knight_effect_length=3,
        animation_mode=AnimationModes.STATIC,
        effect_init=False,
        reverse_animation=False,
        user_animation=None,
        disable_auto_write=False,
        pixels=None,
        refresh_rate=60,
    ):
        if pixels is None:
            import neopixel

            pixels = neopixel.NeoPixel(
                pixel_pin,
                num_pixels,
                pixel_order=rgb_order,
                auto_write=not disable_auto_write,
            )
        self.pixels = pixels
        self.num_pixels = num_pixels

        # PixelBuffer are already iterable, can't do the usual `try: iter(...)`
        if issubclass(self.pixels.__class__, PixelBuf):
            self.pixels = (self.pixels,)

        if self.num_pixels == 0:
            for pixels in self.pixels:
                self.num_pixels += len(pixels)

        if debug.enabled:
            for n, pixels in enumerate(self.pixels):
                debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')

        self.rgbw = bool(len(rgb_order) == 4)

        self.hue_step = hue_step
        self.sat_step = sat_step
        self.val_step = val_step
        self.hue = hue_default
        self.hue_default = hue_default
        self.sat = sat_default
        self.sat_default = sat_default
        self.val = val_default
        self.val_default = val_default
        self.breathe_center = breathe_center
        self.knight_effect_length = knight_effect_length
        self.val_limit = val_limit
        self.animation_mode = animation_mode
        self.animation_speed = animation_speed
        self.effect_init = effect_init
        self.reverse_animation = reverse_animation
        self.user_animation = user_animation
        self.disable_auto_write = disable_auto_write
        self.refresh_rate = refresh_rate

        self._substep = 0

        make_key(
            names=('RGB_TOG',), on_press=self._rgb_tog, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_HUI',), on_press=self._rgb_hui, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_HUD',), on_press=self._rgb_hud, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_SAI',), on_press=self._rgb_sai, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_SAD',), on_press=self._rgb_sad, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_VAI',), on_press=self._rgb_vai, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_VAD',), on_press=self._rgb_vad, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_ANI',), on_press=self._rgb_ani, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_AND',), on_press=self._rgb_and, on_release=handler_passthrough
        )
        make_key(
            names=('RGB_MODE_PLAIN', 'RGB_M_P'),
            on_press=self._rgb_mode_static,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_MODE_BREATHE', 'RGB_M_B'),
            on_press=self._rgb_mode_breathe,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_MODE_RAINBOW', 'RGB_M_R'),
            on_press=self._rgb_mode_rainbow,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'),
            on_press=self._rgb_mode_breathe_rainbow,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_MODE_SWIRL', 'RGB_M_S'),
            on_press=self._rgb_mode_swirl,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_MODE_KNIGHT', 'RGB_M_K'),
            on_press=self._rgb_mode_knight,
            on_release=handler_passthrough,
        )
        make_key(
            names=('RGB_RESET', 'RGB_RST'),
            on_press=self._rgb_reset,
            on_release=handler_passthrough,
        )

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        self._timer = PeriodicTimer(1000 // self.refresh_rate)

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        self.animate()

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        self._do_update()

    def set_hsv(self, hue, sat, val, index):
        '''
        Takes HSV values and displays it on a single LED/Neopixel
        :param hue:
        :param sat:
        :param val:
        :param index: Index of LED/Pixel
        '''

        val = clamp(val, 0, self.val_limit)

        if self.rgbw:
            self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
        else:
            self.set_rgb(hsv_to_rgb(hue, sat, val), index)

    def set_hsv_fill(self, hue, sat, val):
        '''
        Takes HSV values and displays it on all LEDs/Neopixels
        :param hue:
        :param sat:
        :param val:
        '''

        val = clamp(val, 0, self.val_limit)

        if self.rgbw:
            self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
        else:
            self.set_rgb_fill(hsv_to_rgb(hue, sat, val))

    def set_rgb(self, rgb, index):
        '''
        Takes an RGB or RGBW and displays it on a single LED/Neopixel
        :param rgb: RGB or RGBW
        :param index: Index of LED/Pixel
        '''
        if 0 <= index <= self.num_pixels - 1:
            for pixels in self.pixels:
                if index <= (len(pixels) - 1):
                    pixels[index] = rgb
                    break
                index -= len(pixels)

            if not self.disable_auto_write:
                pixels.show()

    def set_rgb_fill(self, rgb):
        '''
        Takes an RGB or RGBW and displays it on all LEDs/Neopixels
        :param rgb: RGB or RGBW
        '''
        for pixels in self.pixels:
            pixels.fill(rgb)
            if not self.disable_auto_write:
                pixels.show()

    def increase_hue(self, step=None):
        '''
        Increases hue by step amount rolling at 256 and returning to 0
        :param step:
        '''
        if step is None:
            step = self.hue_step

        self.hue = (self.hue + step) % 256

        if self._check_update():
            self._do_update()

    def decrease_hue(self, step=None):
        '''
        Decreases hue by step amount rolling at 0 and returning to 256
        :param step:
        '''
        if step is None:
            step = self.hue_step

        if (self.hue - step) <= 0:
            self.hue = (self.hue + 256 - step) % 256
        else:
            self.hue = (self.hue - step) % 256

        if self._check_update():
            self._do_update()

    def increase_sat(self, step=None):
        '''
        Increases saturation by step amount stopping at 255
        :param step:
        '''
        if step is None:
            step = self.sat_step

        self.sat = clamp(self.sat + step, 0, 255)

        if self._check_update():
            self._do_update()

    def decrease_sat(self, step=None):
        '''
        Decreases saturation by step amount stopping at 0
        :param step:
        '''
        if step is None:
            step = self.sat_step

        self.sat = clamp(self.sat - step, 0, 255)

        if self._check_update():
            self._do_update()

    def increase_val(self, step=None):
        '''
        Increases value by step amount stopping at 100
        :param step:
        '''
        if step is None:
            step = self.val_step

        self.val = clamp(self.val + step, 0, 255)

        if self._check_update():
            self._do_update()

    def decrease_val(self, step=None):
        '''
        Decreases value by step amount stopping at 0
        :param step:
        '''
        if step is None:
            step = self.val_step

        self.val = clamp(self.val - step, 0, 255)

        if self._check_update():
            self._do_update()

    def increase_ani(self):
        '''
        Increases animation speed by 1 amount stopping at 10
        :param step:
        '''
        self.animation_speed = clamp(self.animation_speed + 1, 0, 10)

        if self._check_update():
            self._do_update()

    def decrease_ani(self):
        '''
        Decreases animation speed by 1 amount stopping at 0
        :param step:
        '''
        self.animation_speed = clamp(self.animation_speed - 1, 0, 10)

        if self._check_update():
            self._do_update()

    def off(self):
        '''
        Turns off all LEDs/Neopixels without changing stored values
        '''
        self.set_hsv_fill(0, 0, 0)

    def show(self):
        '''
        Turns on all LEDs/Neopixels without changing stored values
        '''
        for pixels in self.pixels:
            pixels.show()

    def animate(self):
        '''
        Activates a "step" in the animation based on the active mode
        :return: Returns the new state in animation
        '''
        if self.effect_init:
            self._init_effect()

        if self.animation_mode is AnimationModes.STATIC_STANDBY:
            return

        if self.enable and self._timer.tick():
            self._animation_step()
            if self.animation_mode == AnimationModes.BREATHING:
                self.effect_breathing()
            elif self.animation_mode == AnimationModes.RAINBOW:
                self.effect_rainbow()
            elif self.animation_mode == AnimationModes.BREATHING_RAINBOW:
                self.effect_breathing_rainbow()
            elif self.animation_mode == AnimationModes.STATIC:
                self.effect_static()
            elif self.animation_mode == AnimationModes.KNIGHT:
                self.effect_knight()
            elif self.animation_mode == AnimationModes.SWIRL:
                self.effect_swirl()
            elif self.animation_mode == AnimationModes.USER:
                self.user_animation(self)
            elif self.animation_mode == AnimationModes.STATIC_STANDBY:
                pass
            else:
                self.off()

    def _animation_step(self):
        self._substep += self.animation_speed / 4
        self._step = int(self._substep)
        self._substep -= self._step

    def _init_effect(self):
        self.pos = 0
        self.reverse_animation = False
        self.effect_init = False

    def _check_update(self):
        return bool(self.animation_mode == AnimationModes.STATIC_STANDBY)

    def _do_update(self):
        if self.animation_mode == AnimationModes.STATIC_STANDBY:
            self.animation_mode = AnimationModes.STATIC

    def effect_static(self):
        self.set_hsv_fill(self.hue, self.sat, self.val)
        self.animation_mode = AnimationModes.STATIC_STANDBY

    def effect_breathing(self):
        # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
        # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
        sined = sin((self.pos / 255.0) * pi)
        multip_1 = exp(sined) - self.breathe_center / e
        multip_2 = self.val_limit / (e - 1 / e)

        self.val = int(multip_1 * multip_2)
        self.pos = (self.pos + self._step) % 256
        self.set_hsv_fill(self.hue, self.sat, self.val)

    def effect_breathing_rainbow(self):
        self.increase_hue(self._step)
        self.effect_breathing()

    def effect_rainbow(self):
        self.increase_hue(self._step)
        self.set_hsv_fill(self.hue, self.sat, self.val)

    def effect_swirl(self):
        self.increase_hue(self._step)
        self.disable_auto_write = True  # Turn off instantly showing
        for i in range(0, self.num_pixels):
            self.set_hsv(
                (self.hue - (i * self.num_pixels)) % 256, self.sat, self.val, i
            )

        # Show final results
        self.disable_auto_write = False  # Resume showing changes
        self.show()

    def effect_knight(self):
        # Determine which LEDs should be lit up
        self.disable_auto_write = True  # Turn off instantly showing
        self.off()  # Fill all off
        pos = int(self.pos)

        # Set all pixels on in range of animation length offset by position
        for i in range(pos, (pos + self.knight_effect_length)):
            self.set_hsv(self.hue, self.sat, self.val, i)

        # Reverse animation when a boundary is hit
        if pos >= self.num_pixels or pos - 1 < (self.knight_effect_length * -1):
            self.reverse_animation = not self.reverse_animation

        if self.reverse_animation:
            self.pos -= self._step / 2
        else:
            self.pos += self._step / 2

        # Show final results
        self.disable_auto_write = False  # Resume showing changes
        self.show()

    def _rgb_tog(self, *args, **kwargs):
        if self.animation_mode == AnimationModes.STATIC:
            self.animation_mode = AnimationModes.STATIC_STANDBY
            self._do_update()
        if self.animation_mode == AnimationModes.STATIC_STANDBY:
            self.animation_mode = AnimationModes.STATIC
            self._do_update()
        if self.enable:
            self.off()
        self.enable = not self.enable

    def _rgb_hui(self, *args, **kwargs):
        self.increase_hue()

    def _rgb_hud(self, *args, **kwargs):
        self.decrease_hue()

    def _rgb_sai(self, *args, **kwargs):
        self.increase_sat()

    def _rgb_sad(self, *args, **kwargs):
        self.decrease_sat()

    def _rgb_vai(self, *args, **kwargs):
        self.increase_val()

    def _rgb_vad(self, *args, **kwargs):
        self.decrease_val()

    def _rgb_ani(self, *args, **kwargs):
        self.increase_ani()

    def _rgb_and(self, *args, **kwargs):
        self.decrease_ani()

    def _rgb_mode_static(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.STATIC

    def _rgb_mode_breathe(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.BREATHING

    def _rgb_mode_breathe_rainbow(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.BREATHING_RAINBOW

    def _rgb_mode_rainbow(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.RAINBOW

    def _rgb_mode_swirl(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.SWIRL

    def _rgb_mode_knight(self, *args, **kwargs):
        self.effect_init = True
        self.animation_mode = AnimationModes.KNIGHT

    def _rgb_reset(self, *args, **kwargs):
        self.hue = self.hue_default
        self.sat = self.sat_default
        self.val = self.val_default
        if self.animation_mode == AnimationModes.STATIC_STANDBY:
            self.animation_mode = AnimationModes.STATIC
        self._do_update()

A  => kmk/extensions/statusled.py +145 -0
@@ 1,145 @@
# Use this extension for showing layer status with three leds

import pwmio
import time

from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_key


class statusLED(Extension):
    def __init__(
        self,
        led_pins,
        brightness=30,
        brightness_step=5,
        brightness_limit=100,
    ):
        self._leds = []
        for led in led_pins:
            try:
                self._leds.append(pwmio.PWMOut(led))
            except Exception as e:
                print(e)
                raise InvalidExtensionEnvironment(
                    'Unable to create pulseio.PWMOut() instance with provided led_pin'
                )
        self._led_count = len(self._leds)

        self.brightness = brightness
        self._layer_last = -1

        self.brightness_step = brightness_step
        self.brightness_limit = brightness_limit

        make_key(names=('SLED_INC',), on_press=self._key_led_inc)
        make_key(names=('SLED_DEC',), on_press=self._key_led_dec)

    def _layer_indicator(self, layer_active, *args, **kwargs):
        '''
        Indicates layer with leds

        For the time being just a simple consecutive single led
        indicator. And when there are more layers than leds it
        wraps around to the first led again.
        (Also works for a single led, which just lights when any
        layer is active)
        '''

        if self._layer_last != layer_active:
            led_last = 0 if self._layer_last == 0 else 1 + (self._layer_last - 1) % 3
            if layer_active > 0:
                led_active = 0 if layer_active == 0 else 1 + (layer_active - 1) % 3
                self.set_brightness(self.brightness, led_active)
                self.set_brightness(0, led_last)
            else:
                self.set_brightness(0, led_last)
            self._layer_last = layer_active

    def __repr__(self):
        return f'SLED({self._to_dict()})'

    def _to_dict(self):
        return {
            '_brightness': self.brightness,
            'brightness_step': self.brightness_step,
            'brightness_limit': self.brightness_limit,
        }

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        '''Light up every single led once for 200 ms'''
        for i in range(self._led_count + 2):
            if i < self._led_count:
                self._leds[i].duty_cycle = int(self.brightness / 100 * 65535)
            i_off = i - 2
            if i_off >= 0 and i_off < self._led_count:
                self._leds[i_off].duty_cycle = int(0)
            time.sleep(0.1)
        for led in self._leds:
            led.duty_cycle = int(0)
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        self._layer_indicator(sandbox.active_layers[0])
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        return

    def on_powersave_enable(self, sandbox):
        self.set_brightness(0)
        return

    def on_powersave_disable(self, sandbox):
        self.set_brightness(self._brightness)
        self._leds[2].duty_cycle = int(50 / 100 * 65535)
        time.sleep(0.2)
        self._leds[2].duty_cycle = int(0)
        return

    def set_brightness(self, percent, layer_id=-1):
        if layer_id < 0:
            for led in self._leds:
                led.duty_cycle = int(percent / 100 * 65535)
        else:
            self._leds[layer_id - 1].duty_cycle = int(percent / 100 * 65535)

    def increase_brightness(self, step=None):
        if not step:
            self._brightness += self.brightness_step
        else:
            self._brightness += step

        if self._brightness > 100:
            self._brightness = 100

        self.set_brightness(self._brightness, self._layer_last)

    def decrease_brightness(self, step=None):
        if not step:
            self._brightness -= self.brightness_step
        else:
            self._brightness -= step

        if self._brightness < 0:
            self._brightness = 0

        self.set_brightness(self._brightness, self._layer_last)

    def _key_led_inc(self, *args, **kwargs):
        self.increase_brightness()

    def _key_led_dec(self, *args, **kwargs):
        self.decrease_brightness()

A  => kmk/extensions/stringy_keymaps.py +45 -0
@@ 1,45 @@
from kmk.extensions import Extension
from kmk.keys import KC


class StringyKeymaps(Extension):
    #####
    # User-configurable
    debug_enabled = False

    def on_runtime_enable(self, keyboard):
        return

    def on_runtime_disable(self, keyboard):
        return

    def during_bootup(self, keyboard):
        for _, layer in enumerate(keyboard.keymap):
            for key_idx, key in enumerate(layer):
                if isinstance(key, str):
                    replacement = KC.get(key)
                    if replacement is None:
                        replacement = KC.NO
                        if self.debug_enabled:
                            print(f"Failed replacing '{key}'. Using KC.NO")
                    elif self.debug_enabled:
                        print(f"Replacing '{key}' with {replacement}")
                    layer[key_idx] = replacement

    def before_matrix_scan(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

A  => kmk/handlers/__init__.py +0 -0
A  => kmk/handlers/sequences.py +155 -0
@@ 1,155 @@
import gc

from kmk.consts import UnicodeMode
from kmk.handlers.stock import passthrough
from kmk.keys import KC, make_key
from kmk.types import AttrDict, KeySequenceMeta


def get_wide_ordinal(char):
    if len(char) != 2:
        return ord(char)

    return 0x10000 + (ord(char[0]) - 0xD800) * 0x400 + (ord(char[1]) - 0xDC00)


def sequence_press_handler(key, keyboard, KC, *args, **kwargs):
    oldkeys_pressed = keyboard.keys_pressed
    keyboard.keys_pressed = set()

    for ikey in key.meta.seq:
        if not getattr(ikey, 'no_press', None):
            keyboard.process_key(ikey, True)
            keyboard._send_hid()
        if not getattr(ikey, 'no_release', None):
            keyboard.process_key(ikey, False)
            keyboard._send_hid()

    keyboard.keys_pressed = oldkeys_pressed

    return keyboard


def simple_key_sequence(seq):
    return make_key(
        meta=KeySequenceMeta(seq),
        on_press=sequence_press_handler,
        on_release=passthrough,
    )


def send_string(message):
    seq = []

    for char in message:
        kc = getattr(KC, char.upper())

        if char.isupper():
            kc = KC.LSHIFT(kc)

        seq.append(kc)

    return simple_key_sequence(seq)


IBUS_KEY_COMBO = simple_key_sequence((KC.LCTRL(KC.LSHIFT(KC.U)),))
RALT_KEY = simple_key_sequence((KC.RALT,))
U_KEY = simple_key_sequence((KC.U,))
ENTER_KEY = simple_key_sequence((KC.ENTER,))
RALT_DOWN_NO_RELEASE = simple_key_sequence((KC.RALT(no_release=True),))
RALT_UP_NO_PRESS = simple_key_sequence((KC.RALT(no_press=True),))


def compile_unicode_string_sequences(string_table):
    '''
    Destructively convert ("compile") unicode strings into key sequences. This
    will, for RAM saving reasons, empty the input dictionary and trigger
    garbage collection.
    '''
    target = AttrDict()

    for k, v in string_table.items():
        target[k] = unicode_string_sequence(v)

    # now loop through and kill the input dictionary to save RAM
    for k in target.keys():
        del string_table[k]

    gc.collect()

    return target


def unicode_string_sequence(unistring):
    '''
    Allows sending things like (╯°□°)╯︵ ┻━┻ directly, without
    manual conversion to Unicode codepoints.
    '''
    return unicode_codepoint_sequence([hex(get_wide_ordinal(s))[2:] for s in 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
    seq = [KC.N0 for _ in range(max(len(codepoint), expected_length))]

    for idx, codepoint_fragment in enumerate(reversed(codepoint)):
        seq[-(idx + 1)] = KC.__getattr__(codepoint_fragment.upper())

    return seq


def unicode_codepoint_sequence(codepoints):
    kc_seqs = (generate_codepoint_keysym_seq(codepoint) for codepoint in codepoints)

    kc_macros = [simple_key_sequence(kc_seq) for kc_seq in kc_seqs]

    def _unicode_sequence(key, keyboard, *args, **kwargs):
        if keyboard.unicode_mode == UnicodeMode.IBUS:
            keyboard.process_key(
                simple_key_sequence(_ibus_unicode_sequence(kc_macros, keyboard)), True
            )
        elif keyboard.unicode_mode == UnicodeMode.RALT:
            keyboard.process_key(
                simple_key_sequence(_ralt_unicode_sequence(kc_macros, keyboard)), True
            )
        elif keyboard.unicode_mode == UnicodeMode.WINC:
            keyboard.process_key(
                simple_key_sequence(_winc_unicode_sequence(kc_macros, keyboard)), True
            )

    return make_key(on_press=_unicode_sequence)


def _ralt_unicode_sequence(kc_macros, keyboard):
    for kc_macro in kc_macros:
        yield RALT_DOWN_NO_RELEASE
        yield kc_macro
        yield RALT_UP_NO_PRESS


def _ibus_unicode_sequence(kc_macros, keyboard):
    for kc_macro in kc_macros:
        yield IBUS_KEY_COMBO
        yield kc_macro
        yield ENTER_KEY


def _winc_unicode_sequence(kc_macros, keyboard):
    '''
    Send unicode sequence using WinCompose:

    http://wincompose.info/
    https://github.com/SamHocevar/wincompose
    '''
    for kc_macro in kc_macros:
        yield RALT_KEY
        yield U_KEY
        yield kc_macro
        yield ENTER_KEY

A  => kmk/handlers/stock.py +129 -0
@@ 1,129 @@
from time import sleep


def passthrough(key, keyboard, *args, **kwargs):
    return keyboard


def default_pressed(key, keyboard, KC, coord_int=None, *args, **kwargs):
    keyboard.hid_pending = True

    keyboard.keys_pressed.add(key)

    return keyboard


def default_released(key, keyboard, KC, coord_int=None, *args, **kwargs):  # NOQA
    keyboard.hid_pending = True
    keyboard.keys_pressed.discard(key)

    return keyboard


def reset(*args, **kwargs):
    import microcontroller

    microcontroller.reset()


def reload(*args, **kwargs):
    import supervisor

    supervisor.reload()


def bootloader(*args, **kwargs):
    import microcontroller

    microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
    microcontroller.reset()


def debug_pressed(key, keyboard, KC, *args, **kwargs):
    if keyboard.debug_enabled:
        print('DebugDisable()')
    else:
        print('DebugEnable()')

    keyboard.debug_enabled = not keyboard.debug_enabled

    return keyboard


def gesc_pressed(key, keyboard, KC, *args, **kwargs):
    GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}

    if GESC_TRIGGERS.intersection(keyboard.keys_pressed):
        # First, release GUI if already pressed
        keyboard._send_hid()
        # if Shift is held, KC_GRAVE will become KC_TILDE on OS level
        keyboard.keys_pressed.add(KC.GRAVE)
        keyboard.hid_pending = True
        return keyboard

    # else return KC_ESC
    keyboard.keys_pressed.add(KC.ESCAPE)
    keyboard.hid_pending = True

    return keyboard


def gesc_released(key, keyboard, KC, *args, **kwargs):
    keyboard.keys_pressed.discard(KC.ESCAPE)
    keyboard.keys_pressed.discard(KC.GRAVE)
    keyboard.hid_pending = True
    return keyboard


def bkdl_pressed(key, keyboard, KC, *args, **kwargs):
    BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}

    if BKDL_TRIGGERS.intersection(keyboard.keys_pressed):
        keyboard._send_hid()
        keyboard.keys_pressed.add(KC.DEL)
        keyboard.hid_pending = True
        return keyboard

    # else return KC_ESC
    keyboard.keys_pressed.add(KC.BKSP)
    keyboard.hid_pending = True

    return keyboard


def bkdl_released(key, keyboard, KC, *args, **kwargs):
    keyboard.keys_pressed.discard(KC.BKSP)
    keyboard.keys_pressed.discard(KC.DEL)
    keyboard.hid_pending = True
    return keyboard


def sleep_pressed(key, keyboard, KC, *args, **kwargs):
    sleep(key.meta.ms / 1000)
    return keyboard


def uc_mode_pressed(key, keyboard, *args, **kwargs):
    keyboard.unicode_mode = key.meta.mode

    return keyboard


def hid_switch(key, keyboard, *args, **kwargs):
    keyboard.hid_type, keyboard.secondary_hid_type = (
        keyboard.secondary_hid_type,
        keyboard.hid_type,
    )
    keyboard._init_hid()
    return keyboard


def ble_refresh(key, keyboard, *args, **kwargs):
    from kmk.hid import HIDModes

    if keyboard.hid_type != HIDModes.BLE:
        return keyboard

    keyboard._hid_helper.stop_advertising()
    keyboard._hid_helper.start_advertising()
    return keyboard

A  => kmk/hid.py +328 -0
@@ 1,328 @@
import supervisor
import usb_hid
from micropython import const

from storage import getmount

from kmk.keys import FIRST_KMK_INTERNAL_KEY, ConsumerKey, ModifierKey

try:
    from adafruit_ble import BLERadio
    from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
    from adafruit_ble.services.standard.hid import HIDService
except ImportError:
    # BLE not supported on this platform
    pass


class HIDModes:
    NOOP = 0  # currently unused; for testing?
    USB = 1
    BLE = 2

    ALL_MODES = (NOOP, USB, BLE)


class HIDReportTypes:
    KEYBOARD = 1
    MOUSE = 2
    CONSUMER = 3
    SYSCONTROL = 4


class HIDUsage:
    KEYBOARD = 0x06
    MOUSE = 0x02
    CONSUMER = 0x01
    SYSCONTROL = 0x80


class HIDUsagePage:
    CONSUMER = 0x0C
    KEYBOARD = MOUSE = SYSCONTROL = 0x01


HID_REPORT_SIZES = {
    HIDReportTypes.KEYBOARD: 8,
    HIDReportTypes.MOUSE: 4,
    HIDReportTypes.CONSUMER: 2,
    HIDReportTypes.SYSCONTROL: 8,  # TODO find the correct value for this
}


class AbstractHID:
    REPORT_BYTES = 8

    def __init__(self, **kwargs):
        self._prev_evt = bytearray(self.REPORT_BYTES)
        self._evt = bytearray(self.REPORT_BYTES)
        self.report_device = memoryview(self._evt)[0:1]
        self.report_device[0] = HIDReportTypes.KEYBOARD

        # Landmine alert for HIDReportTypes.KEYBOARD: byte index 1 of this view
        # is "reserved" and evidently (mostly?) unused. However, other modes (or
        # at least consumer, so far) will use this byte, which is the main reason
        # this view exists. For KEYBOARD, use report_mods and report_non_mods
        self.report_keys = memoryview(self._evt)[1:]

        self.report_mods = memoryview(self._evt)[1:2]
        self.report_non_mods = memoryview(self._evt)[3:]

        self.post_init()

    def __repr__(self):
        return f'{self.__class__.__name__}(REPORT_BYTES={self.REPORT_BYTES})'

    def post_init(self):
        pass

    def create_report(self, keys_pressed):
        self.clear_all()

        consumer_key = None
        for key in keys_pressed:
            if isinstance(key, ConsumerKey):
                consumer_key = key
                break

        reporting_device = self.report_device[0]
        needed_reporting_device = HIDReportTypes.KEYBOARD

        if consumer_key:
            needed_reporting_device = HIDReportTypes.CONSUMER

        if reporting_device != needed_reporting_device:
            # If we are about to change reporting devices, release
            # all keys and close our proverbial tab on the existing
            # device, or keys will get stuck (mostly when releasing
            # media/consumer keys)
            self.send()

        self.report_device[0] = needed_reporting_device

        if consumer_key:
            self.add_key(consumer_key)
        else:
            for key in keys_pressed:
                if key.code >= FIRST_KMK_INTERNAL_KEY:
                    continue

                if isinstance(key, ModifierKey):
                    self.add_modifier(key)
                else:
                    self.add_key(key)

                    if key.has_modifiers:
                        for mod in key.has_modifiers:
                            self.add_modifier(mod)

        return self

    def hid_send(self, evt):
        # Don't raise a NotImplementedError so this can serve as our "dummy" HID
        # when MCU/board doesn't define one to use (which should almost always be
        # the CircuitPython-targeting one, except when unit testing or doing
        # something truly bizarre. This will likely change eventually when Bluetooth
        # is added)
        pass

    def send(self):
        if self._evt != self._prev_evt:
            self._prev_evt[:] = self._evt
            self.hid_send(self._evt)

        return self

    def clear_all(self):
        for idx, _ in enumerate(self.report_keys):
            self.report_keys[idx] = 0x00

        return self

    def clear_non_modifiers(self):
        for idx, _ in enumerate(self.report_non_mods):
            self.report_non_mods[idx] = 0x00

        return self

    def add_modifier(self, modifier):
        if isinstance(modifier, ModifierKey):
            if modifier.code == ModifierKey.FAKE_CODE:
                for mod in modifier.has_modifiers:
                    self.report_mods[0] |= mod
            else:
                self.report_mods[0] |= modifier.code
        else:
            self.report_mods[0] |= modifier

        return self

    def remove_modifier(self, modifier):
        if isinstance(modifier, ModifierKey):
            if modifier.code == ModifierKey.FAKE_CODE:
                for mod in modifier.has_modifiers:
                    self.report_mods[0] ^= mod
            else:
                self.report_mods[0] ^= modifier.code
        else:
            self.report_mods[0] ^= modifier

        return self

    def add_key(self, key):
        # Try to find the first empty slot in the key report, and fill it
        placed = False

        where_to_place = self.report_non_mods

        if self.report_device[0] == HIDReportTypes.CONSUMER:
            where_to_place = self.report_keys

        for idx, _ in enumerate(where_to_place):
            if where_to_place[idx] == 0x00:
                where_to_place[idx] = key.code
                placed = True
                break

        if not placed:
            # TODO what do we do here?......
            pass

        return self

    def remove_key(self, key):
        where_to_place = self.report_non_mods

        if self.report_device[0] == HIDReportTypes.CONSUMER:
            where_to_place = self.report_keys

        for idx, _ in enumerate(where_to_place):
            if where_to_place[idx] == key.code:
                where_to_place[idx] = 0x00

        return self


class USBHID(AbstractHID):
    REPORT_BYTES = 9

    def post_init(self):
        self.devices = {}

        for device in usb_hid.devices:
            us = device.usage
            up = device.usage_page

            if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
                self.devices[HIDReportTypes.CONSUMER] = device
                continue

            if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
                self.devices[HIDReportTypes.KEYBOARD] = device
                continue

            if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
                self.devices[HIDReportTypes.MOUSE] = device
                continue

            if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
                self.devices[HIDReportTypes.SYSCONTROL] = device
                continue

    def hid_send(self, evt):
        if not supervisor.runtime.usb_connected:
            return

        # int, can be looked up in HIDReportTypes
        reporting_device_const = evt[0]

        return self.devices[reporting_device_const].send_report(
            evt[1 : HID_REPORT_SIZES[reporting_device_const] + 1]
        )


class BLEHID(AbstractHID):
    BLE_APPEARANCE_HID_KEYBOARD = const(961)
    # Hardcoded in CPy
    MAX_CONNECTIONS = const(2)

    def __init__(self, ble_name=str(getmount('/').label), **kwargs):
        self.ble_name = ble_name
        super().__init__()

    def post_init(self):
        self.ble = BLERadio()
        self.ble.name = self.ble_name
        self.hid = HIDService()
        self.hid.protocol_mode = 0  # Boot protocol

        # Security-wise this is not right. While you're away someone turns
        # on your keyboard and they can pair with it nice and clean and then
        # listen to keystrokes.
        # On the other hand we don't have LESC so it's like shouting your
        # keystrokes in the air
        if not self.ble.connected or not self.hid.devices:
            self.start_advertising()

    @property
    def devices(self):
        '''Search through the provided list of devices to find the ones with the
        send_report attribute.'''
        if not self.ble.connected:
            return {}

        result = {}

        for device in self.hid.devices:
            if not hasattr(device, 'send_report'):
                continue
            us = device.usage
            up = device.usage_page

            if up == HIDUsagePage.CONSUMER and us == HIDUsage.CONSUMER:
                result[HIDReportTypes.CONSUMER] = device
                continue

            if up == HIDUsagePage.KEYBOARD and us == HIDUsage.KEYBOARD:
                result[HIDReportTypes.KEYBOARD] = device
                continue

            if up == HIDUsagePage.MOUSE and us == HIDUsage.MOUSE:
                result[HIDReportTypes.MOUSE] = device
                continue

            if up == HIDUsagePage.SYSCONTROL and us == HIDUsage.SYSCONTROL:
                result[HIDReportTypes.SYSCONTROL] = device
                continue

        return result

    def hid_send(self, evt):
        if not self.ble.connected:
            return

        # int, can be looked up in HIDReportTypes
        reporting_device_const = evt[0]

        device = self.devices[reporting_device_const]

        report_size = len(device._characteristic.value)
        while len(evt) < report_size + 1:
            evt.append(0)

        return device.send_report(evt[1 : report_size + 1])

    def clear_bonds(self):
        import _bleio

        _bleio.adapter.erase_bonding()

    def start_advertising(self):
        if not self.ble.advertising:
            advertisement = ProvideServicesAdvertisement(self.hid)
            advertisement.appearance = self.BLE_APPEARANCE_HID_KEYBOARD

            self.ble.start_advertising(advertisement)

    def stop_advertising(self):
        self.ble.stop_advertising()

A  => kmk/key_validators.py +9 -0
@@ 1,9 @@
from kmk.types import KeySeqSleepMeta, UnicodeModeKeyMeta


def key_seq_sleep_validator(ms):
    return KeySeqSleepMeta(ms)


def unicode_mode_key_validator(mode):
    return UnicodeModeKeyMeta(mode)

A  => kmk/keys.py +743 -0
@@ 1,743 @@
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):
        if hasattr(self, '_pre_press_handlers'):
            for fn in self._pre_press_handlers:
                if not fn(self, state, KC, coord_int):
                    return None

        ret = self._handle_press(self, state, KC, coord_int)

        if hasattr(self, '_post_press_handlers'):
            for fn in self._post_press_handlers:
                fn(self, state, KC, coord_int)

        return ret

    def on_release(self, state, coord_int=None):
        if hasattr(self, '_pre_release_handlers'):
            for fn in self._pre_release_handlers:
                if not fn(self, state, KC, coord_int):
                    return None

        ret = self._handle_release(self, state, KC, coord_int)

        if hasattr(self, '_post_release_handlers'):
            for fn in self._post_release_handlers:
                fn(self, state, KC, coord_int)

        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)

        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)

        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)

        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)

        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

A  => kmk/kmk_keyboard.py +521 -0
@@ 1,521 @@
try:
    from typing import Callable, Optional, Tuple
except ImportError:
    pass

from supervisor import ticks_ms

from keypad import Event as KeyEvent

from kmk.consts import UnicodeMode
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
from kmk.keys import KC, Key
from kmk.kmktime import ticks_add, ticks_diff
from kmk.modules import Module
from kmk.scanners.keypad import MatrixScanner
from kmk.utils import Debug

debug = Debug(__name__)


class Sandbox:
    matrix_update = None
    secondary_matrix_update = None
    active_layers = None


class KMKKeyboard:
    #####
    # User-configurable
    keymap = []
    coord_mapping = None

    row_pins = None
    col_pins = None
    diode_orientation = None
    matrix = None

    unicode_mode = UnicodeMode.NOOP

    modules = []
    extensions = []
    sandbox = Sandbox()

    #####
    # Internal State
    keys_pressed = set()
    _coordkeys_pressed = {}
    hid_type = HIDModes.USB
    secondary_hid_type = None
    _hid_helper = None
    _hid_send_enabled = False
    hid_pending = False
    matrix_update = None
    secondary_matrix_update = None
    matrix_update_queue = []
    state_changed = False
    _trigger_powersave_enable = False
    _trigger_powersave_disable = False
    i2c_deinit_count = 0
    _go_args = None
    _processing_timeouts = False

    # this should almost always be PREpended to, replaces
    # former use of reversed_active_layers which had pointless
    # overhead (the underlying list was never used anyway)
    active_layers = [0]

    _timeouts = {}

    # on some M4 setups (such as klardotsh/klarank_feather_m4, CircuitPython
    # 6.0rc1) this runs out of RAM every cycle and takes down the board. no
    # real known fix yet other than turning off debug, but M4s have always been
    # tight on RAM so....
    def __repr__(self) -> None:
        return ''.join(
            [
                'KMKKeyboard(\n',
                f'  debug_enabled={self.debug_enabled}, ',
                f'diode_orientation={self.diode_orientation}, ',
                f'matrix={self.matrix},\n',
                f'  unicode_mode={self.unicode_mode}, ',
                f'_hid_helper={self._hid_helper},\n',
                f'  keys_pressed={self.keys_pressed},\n',
                f'  _coordkeys_pressed={self._coordkeys_pressed},\n',
                f'  hid_pending={self.hid_pending}, ',
                f'active_layers={self.active_layers}, ',
                f'_timeouts={self._timeouts}\n',
                ')',
            ]
        )

    def _print_debug_cycle(self, init: bool = False) -> None:
        if debug.enabled:
            debug(f'coordkeys_pressed={self._coordkeys_pressed}')
            debug(f'keys_pressed={self.keys_pressed}')

    def _send_hid(self) -> None:
        if self._hid_send_enabled:
            hid_report = self._hid_helper.create_report(self.keys_pressed)
            try:
                hid_report.send()
            except KeyError as e:
                if debug.enabled:
                    debug(f'HidNotFound(HIDReportType={e})')
        self.hid_pending = False

    def _handle_matrix_report(self, kevent: KeyEvent) -> None:
        if kevent is not None:
            self._on_matrix_changed(kevent)
            self.state_changed = True

    def _find_key_in_map(self, int_coord: int) -> Key:
        try:
            idx = self.coord_mapping.index(int_coord)
        except ValueError:
            if debug.enabled:
                debug(f'CoordMappingNotFound(ic={int_coord})')

            return None

        for layer in self.active_layers:
            try:
                layer_key = self.keymap[layer][idx]
            except IndexError:
                layer_key = None
                if debug.enabled:
                    debug(f'KeymapIndexError(idx={idx}, layer={layer})')

            if not layer_key or layer_key == KC.TRNS:
                continue

            return layer_key

    def _on_matrix_changed(self, kevent: KeyEvent) -> None:
        int_coord = kevent.key_number
        is_pressed = kevent.pressed
        if debug.enabled:
            debug(f'MatrixChange(ic={int_coord}, pressed={is_pressed})')

        key = None
        if not is_pressed:
            try:
                key = self._coordkeys_pressed[int_coord]
            except KeyError:
                if debug.enabled:
                    debug(f'KeyNotPressed(ic={int_coord})')

        if key is None:
            key = self._find_key_in_map(int_coord)

            if key is None:
                if debug.enabled:
                    debug(f'MatrixUndefinedCoordinate(ic={int_coord})')
                return self

        if debug.enabled:
            debug(f'KeyResolution(key={key})')

        self.pre_process_key(key, is_pressed, int_coord)

    @property
    def debug_enabled(self) -> bool:
        return debug.enabled

    @debug_enabled.setter
    def debug_enabled(self, enabled: bool):
        debug.enabled = enabled

    def pre_process_key(
        self,
        key: Key,
        is_pressed: bool,
        int_coord: Optional[int] = None,
        index: int = 0,
    ) -> None:
        for module in self.modules[index:]:
            try:
                key = module.process_key(self, key, is_pressed, int_coord)
                if key is None:
                    break
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.process_key: {err}')

        if int_coord is not None:
            if is_pressed:
                self._coordkeys_pressed[int_coord] = key
            else:
                try:
                    del self._coordkeys_pressed[int_coord]
                except KeyError:
                    if debug.enabled:
                        debug(f'ReleaseKeyError(ic={int_coord})')

        if key:
            self.process_key(key, is_pressed, int_coord)

    def process_key(
        self, key: Key, is_pressed: bool, coord_int: Optional[int] = None
    ) -> None:
        if is_pressed:
            key.on_press(self, coord_int)
        else:
            key.on_release(self, coord_int)

    def resume_process_key(
        self,
        module: Module,
        key: Key,
        is_pressed: bool,
        int_coord: Optional[int] = None,
    ) -> None:
        index = self.modules.index(module) + 1
        self.pre_process_key(key, is_pressed, int_coord, index)

    def remove_key(self, keycode: Key) -> None:
        self.keys_pressed.discard(keycode)
        self.process_key(keycode, False)

    def add_key(self, keycode: Key) -> None:
        self.keys_pressed.add(keycode)
        self.process_key(keycode, True)

    def tap_key(self, keycode: Key) -> None:
        self.add_key(keycode)
        # On the next cycle, we'll remove the key.
        self.set_timeout(False, lambda: self.remove_key(keycode))

    def set_timeout(
        self, after_ticks: int, callback: Callable[[None], None]
    ) -> Tuple[int, int]:
        # We allow passing False as an implicit "run this on the next process timeouts cycle"
        if after_ticks is False:
            after_ticks = 0

        if after_ticks == 0 and self._processing_timeouts:
            after_ticks += 1

        timeout_key = ticks_add(ticks_ms(), after_ticks)

        if timeout_key not in self._timeouts:
            self._timeouts[timeout_key] = []

        idx = len(self._timeouts[timeout_key])
        self._timeouts[timeout_key].append(callback)

        return (timeout_key, idx)

    def cancel_timeout(self, timeout_key: int) -> None:
        try:
            self._timeouts[timeout_key[0]][timeout_key[1]] = None
        except (KeyError, IndexError):
            if debug.enabled:
                debug(f'no such timeout: {timeout_key}')

    def _process_timeouts(self) -> None:
        if not self._timeouts:
            return

        # Copy timeout keys to a temporary list to allow sorting.
        # Prevent net timeouts set during handling from running on the current
        # cycle by setting a flag `_processing_timeouts`.
        current_time = ticks_ms()
        timeout_keys = []
        self._processing_timeouts = True

        for k in self._timeouts.keys():
            if ticks_diff(k, current_time) <= 0:
                timeout_keys.append(k)

        if timeout_keys and debug.enabled:
            debug('processing timeouts')

        for k in sorted(timeout_keys):
            for callback in self._timeouts[k]:
                if callback:
                    callback()
            del self._timeouts[k]

        self._processing_timeouts = False

    def _init_sanity_check(self) -> None:
        '''
        Ensure the provided configuration is *probably* bootable
        '''
        assert self.keymap, 'must define a keymap with at least one row'
        assert (
            self.hid_type in HIDModes.ALL_MODES
        ), 'hid_type must be a value from kmk.consts.HIDModes'
        if not self.matrix:
            assert self.row_pins, 'no GPIO pins defined for matrix rows'
            assert self.col_pins, 'no GPIO pins defined for matrix columns'
            assert (
                self.diode_orientation is not None
            ), 'diode orientation must be defined'

    def _init_coord_mapping(self) -> None:
        '''
        Attempt to sanely guess a coord_mapping if one is not provided. No-op
        if `kmk.extensions.split.Split` is used, it provides equivalent
        functionality in `on_bootup`

        To save RAM on boards that don't use Split, we don't import Split
        and do an isinstance check, but instead do string detection
        '''
        if any(x.__class__.__module__ == 'kmk.modules.split' for x in self.modules):
            return

        if not self.coord_mapping:
            cm = []
            for m in self.matrix:
                cm.extend(m.coord_mapping)
            self.coord_mapping = tuple(cm)

    def _init_hid(self) -> None:
        if self.hid_type == HIDModes.NOOP:
            self._hid_helper = AbstractHID
        elif self.hid_type == HIDModes.USB:
            self._hid_helper = USBHID
        elif self.hid_type == HIDModes.BLE:
            self._hid_helper = BLEHID
        else:
            self._hid_helper = AbstractHID
        self._hid_helper = self._hid_helper(**self._go_args)
        self._hid_send_enabled = True

    def _init_matrix(self) -> None:
        if self.matrix is None:
            if debug.enabled:
                debug('Initialising default matrix scanner.')
            self.matrix = MatrixScanner(
                column_pins=self.col_pins,
                row_pins=self.row_pins,
                columns_to_anodes=self.diode_orientation,
            )

        try:
            self.matrix = tuple(iter(self.matrix))
            offset = 0
            for matrix in self.matrix:
                matrix.offset = offset
                offset += matrix.key_count
        except TypeError:
            self.matrix = (self.matrix,)

    def before_matrix_scan(self) -> None:
        for module in self.modules:
            try:
                module.before_matrix_scan(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.before_matrix_scan: {err}')

        for ext in self.extensions:
            try:
                ext.before_matrix_scan(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {ext}.before_matrix_scan: {err}')

    def after_matrix_scan(self) -> None:
        for module in self.modules:
            try:
                module.after_matrix_scan(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.after_matrix_scan: {err}')

        for ext in self.extensions:
            try:
                ext.after_matrix_scan(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {ext}.after_matrix_scan: {err}')

    def before_hid_send(self) -> None:
        for module in self.modules:
            try:
                module.before_hid_send(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.before_hid_send: {err}')

        for ext in self.extensions:
            try:
                ext.before_hid_send(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(
                        f'Error in {ext}.before_hid_send: {err}',
                    )

    def after_hid_send(self) -> None:
        for module in self.modules:
            try:
                module.after_hid_send(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.after_hid_send: {err}')

        for ext in self.extensions:
            try:
                ext.after_hid_send(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {ext}.after_hid_send: {err}')

    def powersave_enable(self) -> None:
        for module in self.modules:
            try:
                module.on_powersave_enable(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.on_powersave: {err}')

        for ext in self.extensions:
            try:
                ext.on_powersave_enable(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {ext}.powersave_enable: {err}')

    def powersave_disable(self) -> None:
        for module in self.modules:
            try:
                module.on_powersave_disable(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {module}.powersave_disable: {err}')
        for ext in self.extensions:
            try:
                ext.on_powersave_disable(self.sandbox)
            except Exception as err:
                if debug.enabled:
                    debug(f'Error in {ext}.powersave_disable: {err}')

    def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None:
        self._init(hid_type=hid_type, secondary_hid_type=secondary_hid_type, **kwargs)
        while True:
            self._main_loop()

    def _init(
        self,
        hid_type: HIDModes = HIDModes.USB,
        secondary_hid_type: Optional[HIDModes] = None,
        **kwargs,
    ) -> None:
        self._go_args = kwargs
        self.hid_type = hid_type
        self.secondary_hid_type = secondary_hid_type

        self._init_sanity_check()
        self._init_hid()
        self._init_matrix()
        self._init_coord_mapping()

        for module in self.modules:
            try:
                module.during_bootup(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Failed to load module {module}: {err}')
        for ext in self.extensions:
            try:
                ext.during_bootup(self)
            except Exception as err:
                if debug.enabled:
                    debug(f'Failed to load extensions {module}: {err}')

        if debug.enabled:
            debug(f'init: {self}')

    def _main_loop(self) -> None:
        self.state_changed = False
        self.sandbox.active_layers = self.active_layers.copy()

        self.before_matrix_scan()

        for matrix in self.matrix:
            update = matrix.scan_for_changes()
            if update:
                self.matrix_update = update
                break
        self.sandbox.matrix_update = self.matrix_update
        self.sandbox.secondary_matrix_update = self.secondary_matrix_update

        self.after_matrix_scan()

        if self.secondary_matrix_update:
            self.matrix_update_queue.append(self.secondary_matrix_update)
            self.secondary_matrix_update = None

        if self.matrix_update:
            self.matrix_update_queue.append(self.matrix_update)
            self.matrix_update = None

        # only handle one key per cycle.
        if self.matrix_update_queue:
            self._handle_matrix_report(self.matrix_update_queue.pop(0))

        self.before_hid_send()

        if self.hid_pending:
            self._send_hid()

        self._process_timeouts()

        if self.hid_pending:
            self._send_hid()
            self.state_changed = True

        self.after_hid_send()

        if self._trigger_powersave_enable:
            self.powersave_enable()

        if self._trigger_powersave_disable:
            self.powersave_disable()

        if self.state_changed:
            self._print_debug_cycle()

A  => kmk/kmktime.py +34 -0
@@ 1,34 @@
from micropython import const
from supervisor import ticks_ms

_TICKS_PERIOD = const(1 << 29)
_TICKS_MAX = const(_TICKS_PERIOD - 1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)


def ticks_diff(new, start):
    diff = (new - start) & _TICKS_MAX
    diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
    return diff


def ticks_add(ticks, delta):
    return (ticks + delta) % _TICKS_PERIOD


def check_deadline(new, start, ms):
    return ticks_diff(new, start) < ms


class PeriodicTimer:
    def __init__(self, period):
        self.period = period
        self.last_tick = ticks_ms()

    def tick(self):
        now = ticks_ms()
        if ticks_diff(now, self.last_tick) >= self.period:
            self.last_tick = now
            return True
        else:
            return False

A  => kmk/modules/__init__.py +43 -0
@@ 1,43 @@
class InvalidExtensionEnvironment(Exception):
    pass


class Module:
    '''
    Modules differ from extensions in that they not only can read the state, but
    are allowed to modify the state. The will be loaded on boot, and are not
    allowed to be unloaded as they are required to continue functioning in a
    consistant manner.
    '''

    # The below methods should be implemented by subclasses

    def during_bootup(self, keyboard):
        raise NotImplementedError

    def before_matrix_scan(self, keyboard):
        '''
        Return value will be injected as an extra matrix update
        '''
        raise NotImplementedError

    def after_matrix_scan(self, keyboard):
        '''
        Return value will be replace matrix update if supplied
        '''
        raise NotImplementedError

    def process_key(self, keyboard, key, is_pressed, int_coord):
        return key

    def before_hid_send(self, keyboard):
        raise NotImplementedError

    def after_hid_send(self, keyboard):
        raise NotImplementedError

    def on_powersave_enable(self, keyboard):
        raise NotImplementedError

    def on_powersave_disable(self, keyboard):
        raise NotImplementedError

A  => kmk/modules/adns9800.py +241 -0
@@ 1,241 @@
import busio
import digitalio
import microcontroller

import time

from kmk.modules import Module
from kmk.modules.adns9800_firmware import firmware
from kmk.modules.mouse_keys import PointingDevice


class REG:
    Product_ID = 0x0
    Revision_ID = 0x1
    MOTION = 0x2
    DELTA_X_L = 0x3
    DELTA_X_H = 0x4
    DELTA_Y_L = 0x5
    DELTA_Y_H = 0x6
    SQUAL = 0x7
    PIXEL_SUM = 0x8
    Maximum_Pixel = 0x9
    Minimum_Pixel = 0xA
    Shutter_Lower = 0xB
    Shutter_Upper = 0xC
    Frame_Period_Lower = 0xD
    Frame_Period_Upper = 0xE
    Configuration_I = 0xF
    Configuration_II = 0x10
    Frame_Capture = 0x12
    SROM_Enable = 0x13
    Run_Downshift = 0x14
    Rest1_Rate = 0x15
    Rest1_Downshift = 0x16
    Rest2_Rate = 0x17
    Rest2_Downshift = 0x18
    Rest3_Rate = 0x19
    Frame_Period_Max_Bound_Lower = 0x1A
    Frame_Period_Max_Bound_Upper = 0x1B
    Frame_Period_Min_Bound_Lower = 0x1C
    Frame_Period_Min_Bound_Upper = 0x1D
    Shutter_Max_Bound_Lower = 0x1E
    Shutter_Max_Bound_Upper = 0x1F
    LASER_CTRL0 = 0x20
    Observation = 0x24
    Data_Out_Lower = 0x25
    Data_Out_Upper = 0x26
    SROM_ID = 0x2A
    Lift_Detection_Thr = 0x2E
    Configuration_V = 0x2F
    Configuration_IV = 0x39
    Power_Up_Reset = 0x3A
    Shutdown = 0x3B
    Inverse_Product_ID = 0x3F
    Snap_Angle = 0x42
    Motion_Burst = 0x50
    SROM_Load_Burst = 0x62
    Pixel_Burst = 0x64


class ADNS9800(Module):
    tswr = tsww = 120
    tsrw = tsrr = 20
    tsrad = 100
    tbexit = 1
    baud = 2000000
    cpol = 1
    cpha = 1
    DIR_WRITE = 0x80
    DIR_READ = 0x7F

    def __init__(self, cs, sclk, miso, mosi, invert_x=False, invert_y=False):
        self.pointing_device = PointingDevice()
        self.cs = digitalio.DigitalInOut(cs)
        self.cs.direction = digitalio.Direction.OUTPUT
        self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso)
        self.invert_x = invert_x
        self.invert_y = invert_y

    def adns_start(self):
        self.cs.value = False

    def adns_stop(self):
        self.cs.value = True

    def adns_write(self, reg, data):
        while not self.spi.try_lock():
            pass
        try:
            self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
            self.adns_start()
            self.spi.write(bytes([reg | self.DIR_WRITE, data]))
        finally:
            self.spi.unlock()
            self.adns_stop()

    def adns_read(self, reg):
        result = bytearray(1)
        while not self.spi.try_lock():
            pass
        try:
            self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
            self.adns_start()
            self.spi.write(bytes([reg & self.DIR_READ]))
            microcontroller.delay_us(self.tsrad)
            self.spi.readinto(result)
        finally:
            self.spi.unlock()
            self.adns_stop()

        return result[0]

    def adns_upload_srom(self):
        while not self.spi.try_lock():
            pass
        try:
            self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
            self.adns_start()
            self.spi.write(bytes([REG.SROM_Load_Burst | self.DIR_WRITE]))
            for b in firmware:
                self.spi.write(bytes([b]))
        finally:
            self.spi.unlock()
            self.adns_stop()

    def delta_to_int(self, high, low):
        comp = (high << 8) | low
        if comp & 0x8000:
            return (-1) * (0xFFFF + 1 - comp)
        return comp

    def adns_read_motion(self):
        result = bytearray(14)
        while not self.spi.try_lock():
            pass
        try:
            self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
            self.adns_start()
            self.spi.write(bytes([REG.Motion_Burst & self.DIR_READ]))
            microcontroller.delay_us(self.tsrad)
            self.spi.readinto(result)
        finally:
            self.spi.unlock()
            self.adns_stop()
        microcontroller.delay_us(self.tbexit)
        self.adns_write(REG.MOTION, 0x0)
        return result

    def during_bootup(self, keyboard):

        self.adns_write(REG.Power_Up_Reset, 0x5A)
        time.sleep(0.1)
        self.adns_read(REG.MOTION)
        microcontroller.delay_us(self.tsrr)
        self.adns_read(REG.DELTA_X_L)
        microcontroller.delay_us(self.tsrr)
        self.adns_read(REG.DELTA_X_H)
        microcontroller.delay_us(self.tsrr)
        self.adns_read(REG.DELTA_Y_L)
        microcontroller.delay_us(self.tsrr)
        self.adns_read(REG.DELTA_Y_H)
        microcontroller.delay_us(self.tsrw)

        self.adns_write(REG.Configuration_IV, 0x2)
        microcontroller.delay_us(self.tsww)
        self.adns_write(REG.SROM_Enable, 0x1D)
        microcontroller.delay_us(1000)
        self.adns_write(REG.SROM_Enable, 0x18)
        microcontroller.delay_us(self.tsww)

        self.adns_upload_srom()
        microcontroller.delay_us(2000)

        laser_ctrl0 = self.adns_read(REG.LASER_CTRL0)
        microcontroller.delay_us(self.tsrw)
        self.adns_write(REG.LASER_CTRL0, laser_ctrl0 & 0xF0)
        microcontroller.delay_us(self.tsww)
        self.adns_write(REG.Configuration_I, 0x10)
        microcontroller.delay_us(self.tsww)

        if keyboard.debug_enabled:
            print('ADNS: Product ID ', hex(self.adns_read(REG.Product_ID)))
            microcontroller.delay_us(self.tsrr)
            print('ADNS: Revision ID ', hex(self.adns_read(REG.Revision_ID)))
            microcontroller.delay_us(self.tsrr)
            print('ADNS: SROM ID ', hex(self.adns_read(REG.SROM_ID)))
            microcontroller.delay_us(self.tsrr)
            if self.adns_read(REG.Observation) & 0x20:
                print('ADNS: Sensor is running SROM')
            else:
                print('ADNS: Error! Sensor is not runnin SROM!')

        return

    def before_matrix_scan(self, keyboard):
        motion = self.adns_read_motion()
        if motion[0] & 0x80:
            delta_x = self.delta_to_int(motion[3], motion[2])
            delta_y = self.delta_to_int(motion[5], motion[4])

            if self.invert_x:
                delta_x *= -1
            if self.invert_y:
                delta_y *= -1

            if delta_x < 0:
                self.pointing_device.report_x[0] = (delta_x & 0xFF) | 0x80
            else:
                self.pointing_device.report_x[0] = delta_x & 0xFF

            if delta_y < 0:
                self.pointing_device.report_y[0] = (delta_y & 0xFF) | 0x80
            else:
                self.pointing_device.report_y[0] = delta_y & 0xFF

            if keyboard.debug_enabled:
                print('Delta: ', delta_x, ' ', delta_y)
            self.pointing_device.hid_pending = True

        if self.pointing_device.hid_pending:
            keyboard._hid_helper.hid_send(self.pointing_device._evt)
            self.pointing_device.hid_pending = False
            self.pointing_device.report_x[0] = 0
            self.pointing_device.report_y[0] = 0

        return

    def after_matrix_scan(self, keyboard):
        return

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

A  => kmk/modules/capsword.py +99 -0
@@ 1,99 @@
from kmk.keys import FIRST_KMK_INTERNAL_KEY, KC, ModifierKey, make_key
from kmk.modules import Module


class CapsWord(Module):
    # default timeout is 8000
    # alphabets, numbers and few more keys will not disable capsword
    def __init__(self, timeout=8000):
        self._alphabets = range(KC.A.code, KC.Z.code)
        self._numbers = range(KC.N1.code, KC.N0.code)
        self.keys_ignored = [
            KC.MINS,
            KC.BSPC,
            KC.UNDS,
        ]
        self._timeout_key = False
        self._cw_active = False
        self.timeout = timeout
        make_key(
            names=(
                'CAPSWORD',
                'CW',
            ),
            on_press=self.cw_pressed,
        )

    def during_bootup(self, keyboard):
        return

    def before_matrix_scan(self, keyboard):
        return

    def process_key(self, keyboard, key, is_pressed, int_coord):
        if self._cw_active and key != KC.CW:
            continue_cw = False
            # capitalize alphabets
            if key.code in self._alphabets:
                continue_cw = True
                keyboard.process_key(KC.LSFT, is_pressed)
            elif (
                key.code in self._numbers
                or isinstance(key, ModifierKey)
                or key in self.keys_ignored
                or key.code
                >= FIRST_KMK_INTERNAL_KEY  # user defined keys are also ignored
            ):
                continue_cw = True
            # requests and cancels existing timeouts
            if is_pressed:
                if continue_cw:
                    self.discard_timeout(keyboard)
                    self.request_timeout(keyboard)
                else:
                    self.process_timeout()

        return key

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def process_timeout(self):
        self._cw_active = False
        self._timeout_key = False

    def request_timeout(self, keyboard):
        if self._cw_active:
            if self.timeout:
                self._timeout_key = keyboard.set_timeout(
                    self.timeout, lambda: self.process_timeout()
                )

    def discard_timeout(self, keyboard):
        if self._timeout_key:
            if self.timeout:
                keyboard.cancel_timeout(self._timeout_key)
            self._timeout_key = False

    def cw_pressed(self, key, keyboard, *args, **kwargs):
        # enables/disables capsword
        if key == KC.CW:
            if not self._cw_active:
                self._cw_active = True
                self.discard_timeout(keyboard)
                self.request_timeout(keyboard)
            else:
                self.discard_timeout(keyboard)
                self.process_timeout()

A  => kmk/modules/cg_swap.py +70 -0
@@ 1,70 @@
from kmk.keys import KC, ModifierKey, make_key
from kmk.modules import Module


class CgSwap(Module):
    # default cg swap is disabled, can be eanbled too if needed
    def __init__(self, cg_swap_enable=False):
        self.cg_swap_enable = cg_swap_enable
        self._cg_mapping = {
            KC.LCTL: KC.LGUI,
            KC.RCTL: KC.RGUI,
            KC.LGUI: KC.LCTL,
            KC.RGUI: KC.RCTL,
        }
        make_key(
            names=('CG_SWAP',),
        )
        make_key(
            names=('CG_NORM',),
        )
        make_key(
            names=('CG_TOGG',),
        )

    def during_bootup(self, keyboard):
        return

    def matrix_detected_press(self, keyboard):
        return keyboard.matrix_update is None

    def before_matrix_scan(self, keyboard):
        return

    def process_key(self, keyboard, key, is_pressed, int_coord):
        if is_pressed:
            # enables or disables or toggles cg swap
            if key == KC.CG_SWAP:
                self.cg_swap_enable = True
            elif key == KC.CG_NORM:
                self.cg_swap_enable = False
            elif key == KC.CG_TOGG:
                if not self.cg_swap_enable:
                    self.cg_swap_enable = True
                else:
                    self.cg_swap_enable = False
            # performs cg swap
            if (
                self.cg_swap_enable
                and key not in (KC.CG_SWAP, KC.CG_NORM, KC.CG_TOGG)
                and isinstance(key, ModifierKey)
                and key in self._cg_mapping
            ):
                key = self._cg_mapping.get(key)

        return key

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

A  => kmk/modules/combos.py +335 -0
@@ 1,335 @@
try:
    from typing import Optional, Tuple, Union
except ImportError:
    pass
from micropython import const

import kmk.handlers.stock as handlers
from kmk.keys import Key, make_key
from kmk.kmk_keyboard import KMKKeyboard
from kmk.modules import Module


class _ComboState:
    RESET = const(0)
    MATCHING = const(1)
    ACTIVE = const(2)
    IDLE = const(3)


class Combo:
    fast_reset = False
    per_key_timeout = False
    timeout = 50
    _remaining = []
    _timeout = None
    _state = _ComboState.IDLE
    _match_coord = False

    def __init__(
        self,
        match: Tuple[Union[Key, int], ...],
        result: Key,
        fast_reset=None,
        per_key_timeout=None,
        timeout=None,
        match_coord=None,
    ):
        '''
        match: tuple of keys (KC.A, KC.B)
        result: key KC.C
        '''
        self.match = match
        self.result = result
        if fast_reset is not None:
            self.fast_reset = fast_reset
        if per_key_timeout is not None:
            self.per_key_timeout = per_key_timeout
        if timeout is not None:
            self.timeout = timeout
        if match_coord is not None:
            self._match_coord = match_coord

    def __repr__(self):
        return f'{self.__class__.__name__}({[k.code for k in self.match]})'

    def matches(self, key: Key, int_coord: int):
        raise NotImplementedError

    def has_match(self, key: Key, int_coord: int):
        return self._match_coord and int_coord in self.match or key in self.match

    def insert(self, key: Key, int_coord: int):
        if self._match_coord:
            self._remaining.insert(0, int_coord)
        else:
            self._remaining.insert(0, key)

    def reset(self):
        self._remaining = list(self.match)


class Chord(Combo):
    def matches(self, key: Key, int_coord: int):
        if not self._match_coord and key in self._remaining:
            self._remaining.remove(key)
            return True
        elif self._match_coord and int_coord in self._remaining:
            self._remaining.remove(int_coord)
            return True
        else:
            return False


class Sequence(Combo):
    fast_reset = True
    per_key_timeout = True
    timeout = 1000

    def matches(self, key: Key, int_coord: int):
        if (
            not self._match_coord and self._remaining and self._remaining[0] == key
        ) or (
            self._match_coord and self._remaining and self._remaining[0] == int_coord
        ):
            self._remaining.pop(0)
            return True
        else:
            return False


class Combos(Module):
    def __init__(self, combos=[]):
        self.combos = combos
        self._key_buffer = []

        make_key(
            names=('LEADER', 'LDR'),
            on_press=handlers.passthrough,
            on_release=handlers.passthrough,
        )

    def during_bootup(self, keyboard):
        self.reset(keyboard)

    def before_matrix_scan(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

    def process_key(self, keyboard, key: Key, is_pressed, int_coord):
        if is_pressed:
            return self.on_press(keyboard, key, int_coord)
        else:
            return self.on_release(keyboard, key, int_coord)

    def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
        # refill potential matches from timed-out matches
        if self.count_matching() == 0:
            for combo in self.combos:
                if combo._state == _ComboState.RESET:
                    combo._state = _ComboState.MATCHING

        # filter potential matches
        for combo in self.combos:
            if combo._state != _ComboState.MATCHING:
                continue
            if combo.matches(key, int_coord):
                continue
            combo._state = _ComboState.IDLE
            if combo._timeout:
                keyboard.cancel_timeout(combo._timeout)
            combo._timeout = keyboard.set_timeout(
                combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
            )

        match_count = self.count_matching()

        if match_count:
            # At least one combo matches current key: append key to buffer.
            self._key_buffer.append((int_coord, key, True))
            key = None

            for first_match in self.combos:
                if first_match._state == _ComboState.MATCHING:
                    break

            # Single match left: don't wait on timeout to activate
            if match_count == 1 and not any(first_match._remaining):
                combo = first_match
                self.activate(keyboard, combo)
                if combo._timeout:
                    keyboard.cancel_timeout(combo._timeout)
                    combo._timeout = None
                self._key_buffer = []
                self.reset(keyboard)

            # Start or reset individual combo timeouts.
            for combo in self.combos:
                if combo._state != _ComboState.MATCHING:
                    continue
                if combo._timeout:
                    if combo.per_key_timeout:
                        keyboard.cancel_timeout(combo._timeout)
                    else:
                        continue
                combo._timeout = keyboard.set_timeout(
                    combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
                )
        else:
            # There's no matching combo: send and reset key buffer
            self.send_key_buffer(keyboard)
            self._key_buffer = []
            if int_coord is not None:
                key = keyboard._find_key_in_map(int_coord)

        return key

    def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
        for combo in self.combos:
            if combo._state != _ComboState.ACTIVE:
                continue
            if combo.has_match(key, int_coord):
                # Deactivate combo if it matches current key.
                self.deactivate(keyboard, combo)

                if combo.fast_reset:
                    self.reset_combo(keyboard, combo)
                    self._key_buffer = []
                else:
                    combo.insert(key, int_coord)
                    combo._state = _ComboState.MATCHING

                key = combo.result
                break

        else:
            # Non-active but matching combos can either activate on key release
            # if they're the only match, or "un-match" the released key but stay
            # matching if they're a repeatable combo.
            for combo in self.combos:
                if combo._state != _ComboState.MATCHING:
                    continue
                if not combo.has_match(key, int_coord):
                    continue

                # Combo matches, but first key released before timeout.
                elif not any(combo._remaining) and self.count_matching() == 1:
                    keyboard.cancel_timeout(combo._timeout)
                    self.activate(keyboard, combo)
                    self._key_buffer = []
                    keyboard._send_hid()
                    self.deactivate(keyboard, combo)
                    if combo.fast_reset:
                        self.reset_combo(keyboard, combo)
                    else:
                        combo.insert(key, int_coord)
                        combo._state = _ComboState.MATCHING
                    self.reset(keyboard)

                elif not any(combo._remaining):
                    continue

                # Skip combos that allow tapping.
                elif combo.fast_reset:
                    continue

                # This was the last key released of a repeatable combo.
                elif len(combo._remaining) == len(combo.match) - 1:
                    self.reset_combo(keyboard, combo)
                    if not self.count_matching():
                        self.send_key_buffer(keyboard)
                        self._key_buffer = []

                # Anything between first and last key released.
                else:
                    combo.insert(key, int_coord)

            # Don't propagate key-release events for keys that have been
            # buffered. Append release events only if corresponding press is in
            # buffer.
            pressed = self._key_buffer.count((int_coord, key, True))
            released = self._key_buffer.count((int_coord, key, False))
            if (pressed - released) > 0:
                self._key_buffer.append((int_coord, key, False))
                key = None

        # Reset on non-combo key up
        if not self.count_matching():
            self.reset(keyboard)

        return key

    def on_timeout(self, keyboard, combo):
        # If combo reaches timeout and has no remaining keys, activate it;
        # else, drop it from the match list.
        combo._timeout = None

        if not any(combo._remaining):
            self.activate(keyboard, combo)
            # check if the last buffered key event was a 'release'
            if not self._key_buffer[-1][2]:
                keyboard._send_hid()
                self.deactivate(keyboard, combo)
            self._key_buffer = []
            self.reset(keyboard)
        else:
            if self.count_matching() == 1:
                # This was the last pending combo: flush key buffer.
                self.send_key_buffer(keyboard)
                self._key_buffer = []
            self.reset_combo(keyboard, combo)

    def send_key_buffer(self, keyboard):
        for (int_coord, key, is_pressed) in self._key_buffer:
            new_key = None
            if not is_pressed:
                try:
                    new_key = keyboard._coordkeys_pressed[int_coord]
                except KeyError:
                    new_key = None
            if new_key is None:
                new_key = keyboard._find_key_in_map(int_coord)

            keyboard.resume_process_key(self, new_key, is_pressed, int_coord)
            keyboard._send_hid()

    def activate(self, keyboard, combo):
        combo.result.on_press(keyboard)
        combo._state = _ComboState.ACTIVE

    def deactivate(self, keyboard, combo):
        combo.result.on_release(keyboard)
        combo._state = _ComboState.IDLE

    def reset_combo(self, keyboard, combo):
        combo.reset()
        if combo._timeout is not None:
            keyboard.cancel_timeout(combo._timeout)
            combo._timeout = None
        combo._state = _ComboState.RESET

    def reset(self, keyboard):
        for combo in self.combos:
            if combo._state != _ComboState.ACTIVE:
                self.reset_combo(keyboard, combo)

    def count_matching(self):
        match_count = 0
        for combo in self.combos:
            if combo._state == _ComboState.MATCHING:
                match_count += 1
        return match_count

A  => kmk/modules/dynamic_sequences.py +259 -0
@@ 1,259 @@
from micropython import const
from supervisor import ticks_ms

from collections import namedtuple

from kmk.keys import KC, make_argumented_key
from kmk.kmktime import check_deadline, ticks_diff
from kmk.modules import Module


class DSMeta:
    def __init__(self, sequence_select=None):
        self.sequence_select = sequence_select


class SequenceStatus:
    STOPPED = const(0)
    RECORDING = const(1)
    PLAYING = const(2)
    SET_REPEPITIONS = const(3)
    SET_INTERVAL = const(4)


# Keycodes for number keys
_numbers = range(KC.N1.code, KC.N0.code + 1)

SequenceFrame = namedtuple('SequenceFrame', ['keys_pressed', 'timestamp'])


class Sequence:
    def __init__(self):
        self.repetitions = 1
        self.interval = 0
        self.sequence_data = [SequenceFrame(set(), 0) for i in range(3)]


class DynamicSequences(Module):
    def __init__(
        self, slots=1, timeout=60000, key_interval=0, use_recorded_speed=False
    ):
        self.sequences = [Sequence() for i in range(slots)]
        self.current_slot = self.sequences[0]
        self.status = SequenceStatus.STOPPED

        self.index = 0
        self.start_time = 0
        self.current_repetition = 0
        self.last_config_frame = set()

        self.timeout = timeout
        self.key_interval = key_interval
        self.use_recorded_speed = use_recorded_speed

        # Create keycodes
        make_argumented_key(
            validator=DSMeta, names=('RECORD_SEQUENCE',), on_press=self._record_sequence
        )

        make_argumented_key(
            validator=DSMeta, names=('PLAY_SEQUENCE',), on_press=self._play_sequence
        )

        make_argumented_key(
            validator=DSMeta,
            names=(
                'SET_SEQUENCE',
                'STOP_SEQUENCE',
            ),
            on_press=self._stop_sequence,
        )

        make_argumented_key(
            validator=DSMeta,
            names=('SET_SEQUENCE_REPETITIONS',),
            on_press=self._set_sequence_repetitions,
        )

        make_argumented_key(
            validator=DSMeta,
            names=('SET_SEQUENCE_INTERVAL',),
            on_press=self._set_sequence_interval,
        )

    def _record_sequence(self, key, keyboard, *args, **kwargs):
        self._stop_sequence(key, keyboard)
        self.status = SequenceStatus.RECORDING
        self.start_time = ticks_ms()
        self.current_slot.sequence_data = [SequenceFrame(set(), 0)]
        self.index = 0

    def _play_sequence(self, key, keyboard, *args, **kwargs):
        self._stop_sequence(key, keyboard)
        self.status = SequenceStatus.PLAYING
        self.start_time = ticks_ms()
        self.index = 0
        self.current_repetition = 0

    def _stop_sequence(self, key, keyboard, *args, **kwargs):
        if self.status == SequenceStatus.RECORDING:
            self.stop_recording()
        elif self.status == SequenceStatus.SET_INTERVAL:
            self.stop_config()
        self.status = SequenceStatus.STOPPED

        # Change sequences here because stop is always called
        if key.meta.sequence_select is not None:
            self.current_slot = self.sequences[key.meta.sequence_select]

    # Configure repeat settings
    def _set_sequence_repetitions(self, key, keyboard, *args, **kwargs):
        self._stop_sequence(key, keyboard)
        self.status = SequenceStatus.SET_REPEPITIONS
        self.last_config_frame = set()
        self.current_slot.repetitions = 0
        self.start_time = ticks_ms()

    def _set_sequence_interval(self, key, keyboard, *args, **kwargs):
        self._stop_sequence(key, keyboard)
        self.status = SequenceStatus.SET_INTERVAL
        self.last_config_frame = set()
        self.current_slot.interval = 0
        self.start_time = ticks_ms()

    # Add the current keypress state to the sequence
    def record_frame(self, keys_pressed):
        if self.current_slot.sequence_data[self.index].keys_pressed != keys_pressed:
            self.index += 1

            # Recorded speed
            if self.use_recorded_speed:
                self.current_slot.sequence_data.append(
                    SequenceFrame(
                        keys_pressed.copy(), ticks_diff(ticks_ms(), self.start_time)
                    )
                )

            # Constant speed
            else:
                self.current_slot.sequence_data.append(
                    SequenceFrame(keys_pressed.copy(), self.index * self.key_interval)
                )

        if not check_deadline(ticks_ms(), self.start_time, self.timeout):
            self.stop_recording()

    # Add the ending frames to the sequence
    def stop_recording(self):
        # Clear the remaining keys
        self.current_slot.sequence_data.append(
            SequenceFrame(set(), self.current_slot.sequence_data[-1].timestamp + 20)
        )

        # Wait for the specified interval
        prev_timestamp = self.current_slot.sequence_data[-1].timestamp
        self.current_slot.sequence_data.append(
            SequenceFrame(
                set(),
                prev_timestamp + self.current_slot.interval * 1000,
            )
        )

        self.status = SequenceStatus.STOPPED

    def play_frame(self, keyboard):
        # Send the keypresses at this point in the sequence
        if not check_deadline(
            ticks_ms(),
            self.start_time,
            self.current_slot.sequence_data[self.index].timestamp,
        ):
            if self.index:
                prev = self.current_slot.sequence_data[self.index - 1].keys_pressed
                cur = self.current_slot.sequence_data[self.index].keys_pressed

                for key in prev.difference(cur):
                    keyboard.remove_key(key)
                for key in cur.difference(prev):
                    keyboard.add_key(key)

            self.index += 1
            if self.index >= len(self.current_slot.sequence_data):  # Reached the end
                self.current_repetition += 1
                if self.current_repetition == self.current_slot.repetitions:
                    self.status = SequenceStatus.STOPPED
                else:
                    self.index = 0
                    self.start_time = ticks_ms()

    # Configuration for repeating sequences
    def config_mode(self, keyboard):
        for key in keyboard.keys_pressed.difference(self.last_config_frame):
            if key.code in _numbers:
                digit = (key.code - KC.N1.code + 1) % 10
                if self.status == SequenceStatus.SET_REPEPITIONS:
                    self.current_slot.repetitions = (
                        self.current_slot.repetitions * 10 + digit
                    )
                elif self.status == SequenceStatus.SET_INTERVAL:
                    self.current_slot.interval = self.current_slot.interval * 10 + digit

            elif key.code == KC.ENTER.code:
                self.stop_config()

        self.last_config_frame = keyboard.keys_pressed.copy()
        keyboard.hid_pending = False  # Disable typing

        if not check_deadline(ticks_ms(), self.start_time, self.timeout):
            self.stop_config()

    # Finish configuring repetitions
    def stop_config(self):
        self.current_slot.sequence_data[-1] = SequenceFrame(
            self.current_slot.sequence_data[-1].keys_pressed,
            self.current_slot.sequence_data[-2].timestamp
            + self.current_slot.interval * 1000,
        )
        self.current_slot.repetitions = max(self.current_slot.repetitions, 1)
        self.status = SequenceStatus.STOPPED

    def on_runtime_enable(self, keyboard):
        return

    def on_runtime_disable(self, keyboard):
        return

    def during_bootup(self, keyboard):
        return

    def before_matrix_scan(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def before_hid_send(self, keyboard):

        if not self.status:
            return

        elif self.status == SequenceStatus.RECORDING:
            self.record_frame(keyboard.keys_pressed)

        elif self.status == SequenceStatus.PLAYING:
            self.play_frame(keyboard)

        elif (
            self.status == SequenceStatus.SET_REPEPITIONS
            or self.status == SequenceStatus.SET_INTERVAL
        ):
            self.config_mode(keyboard)

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

A  => kmk/modules/easypoint.py +145 -0
@@ 1,145 @@
'''
Extension handles usage of AS5013 by AMS
'''

from supervisor import ticks_ms

from kmk.modules import Module
from kmk.modules.mouse_keys import PointingDevice

I2C_ADDRESS = 0x40
I2X_ALT_ADDRESS = 0x41

X = 0x10
Y_RES_INT = 0x11

XP = 0x12
XN = 0x13
YP = 0x14
YN = 0x15

M_CTRL = 0x2B
T_CTRL = 0x2D

Y_OFFSET = 17
X_OFFSET = 7

DEAD_X = 5
DEAD_Y = 5


class Easypoint(Module):
    '''Module handles usage of AS5013 by AMS'''

    def __init__(
        self,
        i2c,
        address=I2C_ADDRESS,
        y_offset=Y_OFFSET,
        x_offset=X_OFFSET,
        dead_x=DEAD_X,
        dead_y=DEAD_Y,
    ):
        self._i2c_address = address
        self._i2c_bus = i2c

        # HID parameters
        self.pointing_device = PointingDevice()
        self.polling_interval = 20
        self.last_tick = ticks_ms()

        # Offsets for poor soldering
        self.y_offset = y_offset
        self.x_offset = x_offset

        # Deadzone
        self.dead_x = DEAD_X
        self.dead_y = DEAD_Y

    def during_bootup(self, keyboard):
        return

    def before_matrix_scan(self, keyboard):
        '''
        Return value will be injected as an extra matrix update
        '''
        now = ticks_ms()
        if now - self.last_tick < self.polling_interval:
            return
        self.last_tick = now

        x, y = self._read_raw_state()

        # I'm a shit coder, so offset is handled in software side
        s_x = self.getSignedNumber(x, 8) - self.x_offset
        s_y = self.getSignedNumber(y, 8) - self.y_offset

        # Evaluate Deadzone
        if s_x in range(-self.dead_x, self.dead_x) and s_y in range(
            -self.dead_y, self.dead_y
        ):
            # Within bounds, just die
            return
        else:
            # Set the X/Y from easypoint
            self.pointing_device.report_x[0] = x
            self.pointing_device.report_y[0] = y

            self.pointing_device.hid_pending = x != 0 or y != 0

        return

    def after_matrix_scan(self, keyboard):
        return

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        if self.pointing_device.hid_pending:
            keyboard._hid_helper.hid_send(self.pointing_device._evt)
            self._clear_pending_hid()
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

    def _clear_pending_hid(self):
        self.pointing_device.hid_pending = False
        self.pointing_device.report_x[0] = 0
        self.pointing_device.report_y[0] = 0
        self.pointing_device.report_w[0] = 0
        self.pointing_device.button_status[0] = 0

    def _read_raw_state(self):
        '''Read data from AS5013'''
        x, y = self._i2c_rdwr([X], length=2)
        return x, y

    def getSignedNumber(self, number, bitLength=8):
        mask = (2 ** bitLength) - 1
        if number & (1 << (bitLength - 1)):
            return number | ~mask
        else:
            return number & mask

    def _i2c_rdwr(self, data, length=1):
        '''Write and optionally read I2C data.'''
        while not self._i2c_bus.try_lock():
            pass

        try:
            if length > 0:
                result = bytearray(length)
                self._i2c_bus.writeto_then_readfrom(
                    self._i2c_address, bytes(data), result
                )
                return result
            else:
                self._i2c_bus.writeto(self._i2c_address, bytes(data))
            return []
        finally:
            self._i2c_bus.unlock()

A  => kmk/modules/encoder.py +312 -0
@@ 1,312 @@
# See docs/encoder.md for how to use

import busio
import digitalio
from supervisor import ticks_ms

from kmk.modules import Module

# NB : not using rotaryio as it requires the pins to be consecutive


class BaseEncoder:

    VELOCITY_MODE = True

    def __init__(self, is_inverted=False, divisor=4):

        self.is_inverted = is_inverted
        self.divisor = divisor

        self._state = None
        self._start_state = None
        self._direction = None
        self._pos = 0
        self._button_state = True
        self._button_held = None
        self._velocity = 0

        self._movement = 0
        self._timestamp = ticks_ms()

        # callback functions on events. Need to be defined externally
        self.on_move_do = None
        self.on_button_do = None

    def get_state(self):
        return {
            'direction': self.is_inverted and -self._direction or self._direction,
            'position': self.is_inverted and -self._pos or self._pos,
            'is_pressed': not self._button_state,
            'velocity': self._velocity,
        }

        # Called in a loop to refresh encoder state

    def update_state(self):
        # Rotation events
        new_state = (self.pin_a.get_value(), self.pin_b.get_value())

        if new_state != self._state:
            # encoder moved
            self._movement += 1
            # false / false and true / true are common half steps
            # looking on the step just before helps determining
            # the direction
            if new_state[0] == new_state[1] and self._state[0] != self._state[1]:
                if new_state[1] == self._state[0]:
                    self._direction = 1
                else:
                    self._direction = -1

            # when the encoder settles on a position (every 2 steps)
            if new_state[0] == new_state[1]:
                # an encoder returned to the previous
                # position halfway, cancel rotation
                if (
                    self._start_state[0] == new_state[0]
                    and self._start_state[1] == new_state[1]
                    and self._movement <= 2
                ):
                    self._movement = 0
                    self._direction = 0

                # when the encoder made a full loop according to its divisor
                elif self._movement >= self.divisor - 1:
                    # 1 full step is 4 movements (2 for high-resolution encoder),
                    # however, when rotated quickly, some steps may be missed.
                    # This makes it behave more naturally
                    real_movement = self._movement // self.divisor
                    self._pos += self._direction * real_movement
                    if self.on_move_do is not None:
                        for i in range(real_movement):
                            self.on_move_do(self.get_state())

                    # Rotation finished, reset to identify new movement
                    self._movement = 0
                    self._direction = 0
                    self._start_state = new_state

            self._state = new_state

        # Velocity
        self.velocity_event()

        # Button event
        self.button_event()

    def velocity_event(self):
        if self.VELOCITY_MODE:
            new_timestamp = ticks_ms()
            self._velocity = new_timestamp - self._timestamp
            self._timestamp = new_timestamp

    def button_event(self):
        raise NotImplementedError('subclasses must override button_event()!')

    # return knob velocity as milliseconds between position changes (detents)
    # for backwards compatibility
    def vel_report(self):
        # print(self._velocity)
        return self._velocity


class GPIOEncoder(BaseEncoder):
    def __init__(self, pin_a, pin_b, pin_button=None, is_inverted=False, divisor=None):
        super().__init__(is_inverted)

        # Divisor can be 4 or 2 depending on whether the detent
        # on the encoder is defined by 2 or 4 pulses
        self.divisor = divisor

        self.pin_a = EncoderPin(pin_a)
        self.pin_b = EncoderPin(pin_b)
        self.pin_button = (
            EncoderPin(pin_button, button_type=True) if pin_button is not None else None
        )

        self._state = (self.pin_a.get_value(), self.pin_b.get_value())
        self._start_state = self._state

    def button_event(self):
        if self.pin_button:
            new_button_state = self.pin_button.get_value()
            if new_button_state != self._button_state:
                self._button_state = new_button_state
                if self.on_button_do is not None:
                    self.on_button_do(self.get_state())


class EncoderPin:
    def __init__(self, pin, button_type=False):
        self.pin = pin
        self.button_type = button_type
        self.prepare_pin()

    def prepare_pin(self):
        if self.pin is not None:
            self.io = digitalio.DigitalInOut(self.pin)
            self.io.direction = digitalio.Direction.INPUT
            self.io.pull = digitalio.Pull.UP
        else:
            self.io = None

    def get_value(self):
        return self.io.value


class I2CEncoder(BaseEncoder):
    def __init__(self, i2c, address, is_inverted=False):

        try:
            from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
        except ImportError:
            print('seesaw missing')
            return

        super().__init__(is_inverted)

        self.seesaw = seesaw.Seesaw(i2c, address)

        # Check for correct product

        seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
        if seesaw_product != 4991:
            print('Wrong firmware loaded?  Expected 4991')

        self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
        self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
        self.switch = digitalio.DigitalIO(self.seesaw, 24)
        self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)

        self._state = self.encoder.position

    def update_state(self):

        # Rotation events
        new_state = self.encoder.position
        if new_state != self._state:
            # it moves !
            self._movement += 1
            # false / false and true / true are common half steps
            # looking on the step just before helps determining
            # the direction
            if self.encoder.position > self._state:
                self._direction = 1
            else:
                self._direction = -1
            self._state = new_state
            self.on_move_do(self.get_state())

        # Velocity
        self.velocity_event()

        # Button events
        self.button_event()

    def button_event(self):
        if not self.switch.value and not self._button_held:
            # Pressed
            self._button_held = True
            if self.on_button_do is not None:
                self.on_button_do(self.get_state())

        if self.switch.value and self._button_held:
            # Released
            self._button_held = False

    def get_state(self):
        return {
            'direction': self.is_inverted and -self._direction or self._direction,
            'position': self._state,
            'is_pressed': not self.switch.value,
            'is_held': self._button_held,
            'velocity': self._velocity,
        }


class EncoderHandler(Module):
    def __init__(self):
        self.encoders = []
        self.pins = None
        self.map = None
        self.divisor = 4

    def on_runtime_enable(self, keyboard):
        return

    def on_runtime_disable(self, keyboard):
        return

    def during_bootup(self, keyboard):
        if self.pins and self.map:
            for idx, pins in enumerate(self.pins):
                try:
                    # Check for busio.I2C
                    if isinstance(pins[0], busio.I2C):
                        new_encoder = I2CEncoder(*pins)

                    # Else fall back to GPIO
                    else:
                        new_encoder = GPIOEncoder(*pins)
                        # Set default divisor if unset
                        if new_encoder.divisor is None:
                            new_encoder.divisor = self.divisor

                    # In our case, we need to define keybord and encoder_id for callbacks
                    new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do(
                        keyboard, bound_idx, x
                    )
                    new_encoder.on_button_do = (
                        lambda x, bound_idx=idx: self.on_button_do(
                            keyboard, bound_idx, x
                        )
                    )
                    self.encoders.append(new_encoder)
                except Exception as e:
                    print(e)
        return

    def on_move_do(self, keyboard, encoder_id, state):
        if self.map:
            layer_id = keyboard.active_layers[0]
            # if Left, key index 0 else key index 1
            if state['direction'] == -1:
                key_index = 0
            else:
                key_index = 1
            key = self.map[layer_id][encoder_id][key_index]
            keyboard.tap_key(key)

    def on_button_do(self, keyboard, encoder_id, state):
        if state['is_pressed'] is True:
            layer_id = keyboard.active_layers[0]
            key = self.map[layer_id][encoder_id][2]
            keyboard.tap_key(key)

    def before_matrix_scan(self, keyboard):
        '''
        Return value will be injected as an extra matrix update
        '''
        for encoder in self.encoders:
            encoder.update_state()

        return keyboard

    def after_matrix_scan(self, keyboard):
        '''
        Return value will be replace matrix update if supplied
        '''
        return

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):