Skip to content

Commit

Permalink
Remove key pre/post handlers in favor of macros
Browse files Browse the repository at this point in the history
  • Loading branch information
xs5871 committed Jun 8, 2024
1 parent e79414e commit a596e26
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 231 deletions.
114 changes: 16 additions & 98 deletions docs/en/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ you're stumped: [`kmk/keys.py`](/kmk/keys.py).

---

## Key Objects

This is a bunch of documentation about how a physical keypress translates to
events (and the lifecycle of said events) in KMK. It's somewhat technical, but
if you're looking to extend your keyboard's functionality with extra code,
Expand Down Expand Up @@ -41,9 +43,6 @@ objects have a few core pieces of information:
functions and some special override keys (like `KC.GESC`, which is an enhanced
form of existing ANSI keys) in [`kmk/handlers/stock.py`](/kmk/handlers/stock.py).

- Optional callbacks to be run before and/or after the above handlers. More on
that soon.

- A generic `meta` field, which is most commonly used for "argumented" keys -
objects in the `KC` object which are actually functions that return `Key`
instances, which often need to access the arguments passed into the "outer"
Expand All @@ -63,101 +62,20 @@ keyboard.keymap = [ ... CTRLSHFT ... ]
```

When a key is pressed and we've pulled a `Key` object out of the keymap, the
following will happen:

- Pre-press callbacks will be run in the order they were assigned, with their
return values discarded (unless the user attached these, they will almost
never exist)
- The assigned press handler will be run (most commonly, this is provided by
KMK)
- Post-press callbacks will be run in the order they were assigned, with their
return values discarded (unless the user attached these, they will almost
never exist)

These same steps are run for when a key is released.

_So now... what's a handler, and what's a pre/post callback?!_

All of these serve roughly the same purpose: to _do something_ with the key's
data, or to fire off side effects. Most handlers are provided by KMK internally
and modify the `InternalState` in some way - adding the key to the HID queue,
changing layers, etc. The pre/post handlers are designed to allow functionality
to be bolted on at these points in the event flow without having to reimplement
(or import and manually call) the internal handlers.

All of these methods take the same arguments, and for this, I'll lift a
docstring straight out of the source:

> Receives the following:
>
> - self (this Key instance)
> - state (the current InternalState)
> - KC (the global KC lookup table, for convenience)
> - `coord_int` (an internal integer representation of the matrix coordinate
> for the pressed key - this is likely not useful to end users, but is
> provided for consistency with the internal handlers)
> - `coord_raw` (an X,Y tuple of the matrix coordinate - also likely not useful)
>
> The return value of the provided callback is discarded. Exceptions are _not_
> caught, and will likely crash KMK if not handled within your function.
>
> These handlers are run in attachment order: handlers provided by earlier
> calls of this method will be executed before those provided by later calls.
This means if you want to add things like underglow/LED support, or have a
button that triggers your GSM modem to call someone, or whatever else you can
hack up in CircuitPython, which also retaining layer-switching abilities or
whatever the stock handler is, you're covered. This also means you can add
completely new functionality to KMK by writing your own handler.

Here's an example of an after_press_handler to change the RGB lights with a layer change:

```python
LOWER = KC.DF(LYR_LOWER) #Set layer to LOWER

def low_lights(key, keyboard, *args):
print('Lower Layer') #serial feedback
keyboard.pixels.set_hsv_fill(0, 100, 255) #RGB extension call to set (H,S,V) values

LOWER.after_press_handler(low_lights) #call the key with the after_press_handler
```

Here's an example of a lifecycle hook to print a giant Shrek ASCII art. It
doesn't care about any of the arguments passed into it, because it has no
intentions of modifying the internal state. It is purely a [side
effect](<https://en.wikipedia.org/wiki/Side_effect_(computer_science)>) run every
time Left Alt is pressed:

```python
def shrek(*args, **kwargs):
print('⢀⡴⠑⡄⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀')
print('⠸⡇⠀⠿⡀⠀⠀⠀⣀⡴⢿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠑⢄⣠⠾⠁⣀⣄⡈⠙⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⢀⡀⠁⠀⠀⠈⠙⠛⠂⠈⣿⣿⣿⣿⣿⠿⡿⢿⣆⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⢀⡾⣁⣀⠀⠴⠂⠙⣗⡀⠀⢻⣿⣿⠭⢤⣴⣦⣤⣹⠀⠀⠀⢀⢴⣶⣆')
print('⠀⠀⢀⣾⣿⣿⣿⣷⣮⣽⣾⣿⣥⣴⣿⣿⡿⢂⠔⢚⡿⢿⣿⣦⣴⣾⠁⠸⣼⡿')
print('⠀⢀⡞⠁⠙⠻⠿⠟⠉⠀⠛⢹⣿⣿⣿⣿⣿⣌⢤⣼⣿⣾⣿⡟⠉⠀⠀⠀⠀⠀')
print('⠀⣾⣷⣶⠇⠀⠀⣤⣄⣀⡀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀')
print('⠀⠉⠈⠉⠀⠀⢦⡈⢻⣿⣿⣿⣶⣶⣶⣶⣤⣽⡹⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⠀⠉⠲⣽⡻⢿⣿⣿⣿⣿⣿⣿⣷⣜⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣷⣶⣮⣭⣽⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀')
print('⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⠿⠿⠿⠿⠛⠉')

return False #Returning True will follow thru the normal handlers sending the ALT key to the OS
KC.LALT.before_press_handler(shrek)
```

You can also copy a key without any pre/post handlers attached with `.clone()`,
so for example, if I've already added Shrek to my `LALT` but want a Shrek-less
`LALT` key elsewhere in my keymap, I can just clone it, and the new key won't
have my handlers attached:

```python
SHREKLESS_ALT = KC.LALT.clone()
```
`Key` is first passed through the module processing pipeline.
Modules can do whatever with that `Key`, but usually keys either pass right
through, or are intercepted and emitted again later (think of timing based
modules like Combos and Hold-Tap).
Finally the assigned press handler will be run (most commonly, this is provided
by KMK).
On release the `Key` object lookup is, most of the time, cached and doesn't
require searching the keymap again.
Then it's the processing pipeline again, followed by the release handler.

Custom behavior can either be achieved with custom press and release handlers or
with [macros](docs/en/macros.md).

## The Key Code Dictionary

You can also refer to a key by index:

Expand Down
139 changes: 6 additions & 133 deletions kmk/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,8 @@ def __init__(
self.has_modifiers = has_modifiers
# cast to bool() in case we get a None value

self._handle_press = on_press
self._handle_release = on_release
self._on_press = on_press
self._on_release = on_release
self.meta = meta

def __call__(self) -> Key:
Expand All @@ -476,137 +476,10 @@ def __repr__(self):
return f'Key(code={self.code}, has_modifiers={self.has_modifiers})'

def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
if hasattr(self, '_pre_press_handlers'):
for fn in self._pre_press_handlers:
if not fn(self, keyboard, KC, coord_int):
return

self._handle_press(self, keyboard, KC, coord_int)

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

def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
if hasattr(self, '_pre_release_handlers'):
for fn in self._pre_release_handlers:
if not fn(self, keyboard, KC, coord_int):
return

self._handle_release(self, keyboard, KC, coord_int)

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

def clone(self) -> Key:
'''
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,
on_press=self._handle_press,
on_release=self._handle_release,
meta=self.meta,
)

def before_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
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)

def after_press_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
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)

def before_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
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)

def after_release_handler(self, fn: Callable[[Key, Keyboard, ...], bool]) -> None:
'''
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)
self._on_release(self, keyboard, KC, coord_int)


class ModifierKey(Key):
Expand Down Expand Up @@ -637,8 +510,8 @@ def __call__(
return type(modified_key)(
code=code,
has_modifiers=modifiers,
on_press=modified_key._handle_press,
on_release=modified_key._handle_release,
on_press=modified_key._on_press,
on_release=modified_key._on_release,
meta=modified_key.meta,
)

Expand Down

0 comments on commit a596e26

Please sign in to comment.