Skip to content

Commit

Permalink
Merge pull request #105 from KMKfw/topic-refactor-keycodes-and-macros
Browse files Browse the repository at this point in the history
Congressional Bill 122918 Forgot To Sleep Edition: Refactor everything about how key definitions work
  • Loading branch information
klardotsh authored Feb 21, 2019
2 parents af140a1 + d4f4872 commit 1ad7602
Show file tree
Hide file tree
Showing 27 changed files with 1,568 additions and 1,418 deletions.
6 changes: 1 addition & 5 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
Almost all of KMK is licensed under the GPLv3. There are a couple of
exceptions:
Almost all of KMK is licensed under the GPLv3. The only exceptions are:

- `kmk/string.py` is copied directly from
[micropython-lib](https://github.com/micropython/micropython-lib) and is
under the MIT license, copyrighted by the micropython-lib contributors
- Hardware schematics are licensed under individual terms per schematic

Files/components not listed above or containing its own copyright header in the
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ AMPY_DELAY ?= 1.5
ARDUINO ?= /usr/share/arduino
PIPENV ?= $(shell which pipenv)

all: copy-kmk copy-keymap
all: copy-kmk copy-bootpy copy-keymap

.docker_base: Dockerfile_base
@echo "===> Building Docker base image kmkfw/base:${DOCKER_BASE_TAG}"
Expand Down Expand Up @@ -78,6 +78,18 @@ copy-kmk:
echo "**** MOUNTPOINT must be defined (wherever your CIRCUITPY drive is mounted) ****" && exit 1
endif

ifdef MOUNTPOINT
$(MOUNTPOINT)/kmk/boot.py: boot.py
@echo "===> Copying required boot.py"
@rsync -rh boot.py $(MOUNTPOINT)/
@sync

copy-bootpy: $(MOUNTPOINT)/kmk/boot.py
else
copy-bootpy:
echo "**** MOUNTPOINT must be defined (wherever your CIRCUITPY drive is mounted) ****" && exit 1
endif

ifdef MOUNTPOINT
ifndef USER_KEYMAP
$(MOUNTPOINT)/main.py:
Expand Down
3 changes: 3 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import supervisor

supervisor.set_next_stack_limit(4096 + 1024)
48 changes: 3 additions & 45 deletions docs/keycodes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Keycodes Overview
# Keys Overview

## [Basic Keycodes]
## [Basic Keys]

|Key |Aliases |Description |
|-----------------------|--------------------|-----------------------------------------------|
Expand Down Expand Up @@ -103,8 +103,6 @@
|`KC.KP_0` |`KC.P0` |Keypad `0` and Insert |
|`KC.KP_DOT` |`KC.PDOT` |Keypad `.` and Delete |
|`KC.NONUS_BSLASH` |`KC.NUBS` |Non-US `\` and <code>&#124;</code> |
|`KC.APPLICATION` |`KC.APP` |Application (Windows Menu Key) |
|`KC.POWER` | |System Power (macOS) |
|`KC.KP_EQUAL` |`KC.PEQL` |Keypad `=` |
|`KC.F13` | |F13 |
|`KC.F14` | |F14 |
Expand All @@ -118,20 +116,6 @@
|`KC.F22` | |F22 |
|`KC.F23` | |F23 |
|`KC.F24` | |F24 |
|`KC.EXECUTE` |`KC.EXEC` |Execute |
|`KC.HELP` | |Help |
|`KC.MENU` | |Menu |
|`KC.SELECT` |`KC.SLCT` |Select |
|`KC.STOP` | |Stop |
|`KC.AGAIN` |`KC.AGIN` |Again |
|`KC.UNDO` | |Undo |
|`KC.CUT` | |Cut |
|`KC.COPY` | |Copy |
|`KC.PASTE` |`KC.PSTE` |Paste |
|`KC.FIND` | |Find |
|`KC._MUTE` | |Mute (macOS) |
|`KC._VOLUP` | |Volume Up (macOS) |
|`KC._VOLDOWN` | |Volume Down (macOS) |
|`KC.LOCKING_CAPS` |`KC.LCAP` |Locking Caps Lock |
|`KC.LOCKING_NUM` |`KC.LNUM` |Locking Num Lock |
|`KC.LOCKING_SCROLL` |`KC.LSCR` |Locking Scroll Lock |
Expand All @@ -155,18 +139,6 @@
|`KC.LANG7` | |Language 7 |
|`KC.LANG8` | |Language 8 |
|`KC.LANG9` | |Language 9 |
|`KC.ALT_ERASE` |`KC.ERAS` |Alternate Erase |
|`KC.SYSREQ` | |SysReq/Attention |
|`KC.CANCEL` | |Cancel |
|`KC.CLEAR` |`KC.CLR` |Clear |
|`KC.PRIOR` | |Prior |
|`KC.RETURN` | |Return |
|`KC.SEPARATOR` | |Separator |
|`KC.OUT` | |Out |
|`KC.OPER` | |Oper |
|`KC.CLEAR_AGAIN` | |Clear/Again |
|`KC.CRSEL` | |CrSel/Props |
|`KC.EXSEL` | |ExSel |
|`KC.LCTRL` |`KC.LCTL` |Left Control |
|`KC.LSHIFT` |`KC.LSFT` |Left Shift |
|`KC.LALT` | |Left Alt |
Expand All @@ -175,28 +147,14 @@
|`KC.RSHIFT` |`KC.RSFT` |Right Shift |
|`KC.RALT` | |Right Alt |
|`KC.RGUI` |`KC.RCMD`, `KC.RWIN`|Right GUI (Windows/Command/Meta key) |
|`KC.SYSTEM_POWER` |`KC.PWR` |System Power Down |
|`KC.SYSTEM_SLEEP` |`KC.SLEP` |System Sleep |
|`KC.SYSTEM_WAKE` |`KC.WAKE` |System Wake |
|`KC.AUDIO_MUTE` |`KC.MUTE` |Mute |
|`KC.AUDIO_VOL_UP` |`KC.VOLU` |Volume Up |
|`KC.AUDIO_VOL_DOWN` |`KC.VOLD` |Volume Down |
|`KC.MEDIA_NEXT_TRACK` |`KC.MNXT` |Next Track (Windows) |
|`KC.MEDIA_PREV_TRACK` |`KC.MPRV` |Previous Track (Windows) |
|`KC.MEDIA_STOP` |`KC.MSTP` |Stop Track (Windows) |
|`KC.MEDIA_PLAY_PAUSE` |`KC.MPLY` |Play/Pause Track |
|`KC.MEDIA_SELECT` |`KC.MSEL` |Launch Media Player (Windows) |
|`KC.MEDIA_EJECT` |`KC.EJCT` |Eject (macOS) |
|`KC.MAIL` | |Launch Mail (Windows) |
|`KC.CALCULATOR` |`KC.CALC` |Launch Calculator (Windows) |
|`KC.MY_COMPUTER` |`KC.MYCM` |Launch My Computer (Windows) |
|`KC.WWW_SEARCH` |`KC.WSCH` |Browser Search (Windows) |
|`KC.WWW_HOME` |`KC.WHOM` |Browser Home (Windows) |
|`KC.WWW_BACK` |`KC.WBAK` |Browser Back (Windows) |
|`KC.WWW_FORWARD` |`KC.WFWD` |Browser Forward (Windows) |
|`KC.WWW_STOP` |`KC.WSTP` |Browser Stop (Windows) |
|`KC.WWW_REFRESH` |`KC.WREF` |Browser Refresh (Windows) |
|`KC.WWW_FAVORITES` |`KC.WFAV` |Browser Favorites (Windows) |
|`KC.MEDIA_FAST_FORWARD`|`KC.MFFD` |Next Track (macOS) |
|`KC.MEDIA_REWIND` |`KC.MRWD` |Previous Track (macOS) |

Expand Down Expand Up @@ -228,7 +186,7 @@
|`KC.QUESTION` |`KC.QUES` |`?` |


## [Internal Keycodes]
## [Internal Keys]

|Key |Description |
|-----------------------|---------------------------------------------------------------------|
Expand Down
158 changes: 158 additions & 0 deletions docs/keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Keys

> NOTE: This is not a lookup table of key objects provided by KMK. That listing
> can be found in `keycodes.md`, though that file is not always kept up to date.
> It's probably worth a look at the raw source if you're stumped: `kmk/keys.py`.
This is a bunch of documentation about how physical keypresses translate 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,
you'll need at least some of this technical knowledge.

The first few steps in the process aren't all that interesting for most
workflows, which is why they're buried deep in KMK: we scan a bunch of GPIO
lanes (about as quickly as CircuitPython will let us) to see where, in a matrix
of keys, a key has been pressed. The technical details about this process [are
probably best left to
Wikipedia](https://en.wikipedia.org/wiki/Keyboard_matrix_circuit). Then, we scan
through the defined keymap, finding the first valid key at this index based on
the stack of currently active layers (this logic, if you want to read through
the code, is in `kmk/internal_state.py`, method `_find_key_in_map`).

The next few steps are the interesting part, but to understand them, we need to
understand a bit about what a `Key` object is (found in `kmk/keys.py`). `Key`
objects have a few core pieces of information:

* Their `code`, which can be any integer. Integers below
`FIRST_KMK_INTERNAL_KEY` are sent through to the HID stack (and thus the
computer, which will translate that integer to something meaningful - for
example, `code=4` becomes `a` on a US QWERTY/Dvorak keyboard).

* Their attached modifiers (to implement things like shifted keys or `KC.HYPR`,
which are single key presses sending along more than one key in a single HID
report. This is a distinct concept from Sequences, which are a KMK feature
documented in `sequences.md`). For almost all purposes outside of KMK core,
this field should be ignored - it can be safely populated through far more
sane means than futzing with it by hand.

* Some data on whether the key should actually be pressed or released - this is
mostly an implementation detail of how Sequences work, where, for example,
`KC.RALT` may need to be held down for the entirety of a sequence, rather than
being released immediately before moving to the next character. Usually end
users shouldn't need to mess with this, but the fields are called `no_press`
and `no_release` and are referenced in a few places in the codebase if you
need examples.

* Handlers for "press" (sometimes known as "keydown") and "release" (sometimes
known as "keyup") events. KMK provides handlers for standard keyboard
functions and some special override keys (like `KC.GESC`, which is an enhanced
form of existing ANSI keys) in `kmk/handlers/stock.py`, for layer switching in
`kmk/handlers.layers.py`, and for everything related to Sequences (see
`sequences.md` again) in `kmk/handlers/sequences.py`. We'll discuss these more
shortly.

* 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"
function. Many of these examples are related to layer switching - for example,
`KC.MO` is implemented as an argumented key - when the user adds `KC.MO(1)` to
their keymap, the function call returns a `Key` object with `meta` set to an
object containing `layer` and `kc` properties, for example. There's other uses
for `meta`, and examples can be found in `kmk/types.py`

`Key` objects can also be chained together by calling them! To create a key
which holds Control and Shift simultaneously, we can simply do:

```python
CTRLSHFT = KC.LCTL(KC.LSFT)

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 rougly 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 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('⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⠿⠿⠿⠿⠛⠉')


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()
```
Loading

0 comments on commit 1ad7602

Please sign in to comment.