Browse Source

Macros in JSON keymaps (#14374)

* macros in json keymaps

* add advanced macro support to json

* add a note about escaping macro strings

* add simple examples

* format json

* add support for language specific keymap extras

* switch to dictionaries instead of inline text for macros

* use SS_TAP on the innermost tap keycode

* add the new macro format to the schema

* document the macro limit

* add the json keyword for syntax highlighting

* fix format that vscode screwed up

* Update feature_macros.md

* add tests for macros

* change ding to beep

* add json support for SENDSTRING_BELL

* update doc based on feedback from sigprof

* document host_layout

* remove unused var

* improve carriage return handling

* support tab characters as well

* Update docs/feature_macros.md

Co-authored-by: Nick Brassel <nick@tzarc.org>

* escape backslash characters

* format

* flake8

* Update quantum/quantum_keycodes.h

Co-authored-by: Nick Brassel <nick@tzarc.org>
master
Zach White 7 months ago
committed by GitHub
parent
commit
08ce0142ba
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      data/mappings/info_config.json
  2. 1
      data/schemas/keyboard.jsonschema
  3. 35
      data/schemas/keymap.jsonschema
  4. 134
      docs/feature_macros.md
  5. 0
      keyboards/handwired/pytest/macro/.noci
  6. 10
      keyboards/handwired/pytest/macro/info.json
  7. 15
      keyboards/handwired/pytest/macro/keymaps/default/keymap.json
  8. 0
      keyboards/handwired/pytest/macro/readme.md
  9. 1
      keyboards/handwired/pytest/macro/rules.mk
  10. 2
      lib/python/qmk/cli/json2c.py
  11. 2
      lib/python/qmk/commands.py
  12. 100
      lib/python/qmk/keymap.py
  13. 8
      lib/python/qmk/tests/test_cli_commands.py
  14. 8
      lib/python/qmk/tests/test_qmk_keymap.py
  15. 34
      quantum/quantum_keycodes.h
  16. 1
      requirements-dev.txt

1
data/mappings/info_config.json

@ -76,6 +76,7 @@
"QMK_KEYS_PER_SCAN": {"info_key": "qmk.keys_per_scan", "value_type": "int"},
"QMK_LED": {"info_key": "qmk_lufa_bootloader.led"},
"QMK_SPEAKER": {"info_key": "qmk_lufa_bootloader.speaker"},
"SENDSTRING_BELL": {"info_key": "audio.macro_beep", "value_type": "bool"},
"SPLIT_MODS_ENABLE": {"info_key": "split.transport.sync_modifiers", "value_type": "bool"},
"SPLIT_TRANSPORT_MIRROR": {"info_key": "split.transport.sync_matrix_state", "value_type": "bool"},
"SPLIT_USB_DETECT": {"info_key": "split.usb_detect.enabled", "value_type": "bool"},

1
data/schemas/keyboard.jsonschema

@ -19,6 +19,7 @@
"type": "object",
"additionalProperties": false,
"properties": {
"macro_beep": {"type": "boolean"},
"pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"},
"voices": {"type": "boolean"}
}

35
data/schemas/keymap.jsonschema

@ -5,6 +5,7 @@
"type": "object",
"properties": {
"author": {"type": "string"},
"host_language": {"$ref": "qmk.definitions.v1#/text_identifier"},
"keyboard": {"$ref": "qmk.definitions.v1#/text_identifier"},
"keymap": {"$ref": "qmk.definitions.v1#/text_identifier"},
"layout": {"$ref": "qmk.definitions.v1#/layout_macro"},
@ -15,10 +16,42 @@
"items": {"type": "string"}
}
},
"macros": {
"type": "array",
"items": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"action": {
"type": "string",
"enum": ['beep', 'delay', 'down', 'tap', 'up']
},
"keycodes": {
"type": "array",
"items": {
"$ref": "qmk.definitions.v1#/text_identifier"
}
},
"duration": {
"$ref": "qmk.definitions.v1#/unsigned_int"
}
}
}
]
}
}
},
"config": {"$ref": "qmk.keyboard.v1"},
"notes": {
"type": "string",
"description": "asdf"
}
}
}
}

134
docs/feature_macros.md

@ -4,7 +4,107 @@ Macros allow you to send multiple keystrokes when pressing just one key. QMK has
!> **Security Note**: While it is possible to use macros to send passwords, credit card numbers, and other sensitive information it is a supremely bad idea to do so. Anyone who gets a hold of your keyboard will be able to access that information by opening a text editor.
## `SEND_STRING()` & `process_record_user`
## Using Macros In JSON Keymaps
You can define up to 32 macros in a `keymap.json` file, as used by [Configurator](newbs_building_firmware_configurator.md), and `qmk compile`. You can define these macros in a list under the `macros` keyword, like this:
```json
{
"keyboard": "handwired/my_macropad",
"keymap": "my_keymap",
"macros": [
[
{"action":"down", "keycodes": ["LSFT"]},
"hello world1",
{"action": "up","keycodes": ["LSFT"]}
],
[
{"action":"tap", "keycodes": ["LCTL", "LALT", "DEL"]}
],
[
"ding!",
{"action":"beep"}
],
[
{"action":"tap", "keycodes": ["F1"]},
{"action":"delay", "duration": "1000"},
{"action":"tap", "keycodes": ["PGDN"]}
]
],
"layout": "LAYOUT_all",
"layers": [
["MACRO_0", "MACRO_1", "MACRO_2", "MACRO_3"]
]
}
```
### Selecting Your Host Keyboard Layout
If you type in a language other than English, or use a non-QWERTY layout like Colemak, Dvorak, or Workman, you may have set your computer's input language to match this layout. This presents a challenge when creating macros- you may need to type different keys to get the same letters! To address this you can add the `host_language` key to your keymap.json, like so:
```json
{
"keyboard": "handwired/my_macropad",
"keymap": "my_keymap",
"host_layout": "dvorak",
"macros": [
["Hello, World!"]
],
"layout": "LAYOUT_all",
"layers": [
["MACRO_0"]
]
}
```
The current list of available languages is:
| belgian | bepo | br_abnt2 | canadian_multilingual |
|:-------:|:----:|:--------:|:---------------------:|
| **colemak** | **croatian** | **czech** | **danish** |
| **dvorak_fr** | **dvorak** | **dvp** | **estonian** |
| **finnish** | **fr_ch** | **french_afnor** | **french** |
| **french_osx** | **german_ch** | **german** | **german_osx** |
| **hungarian** | **icelandic** | **italian** | **italian_osx_ansi** |
| **italian_osx_iso** | **jis** | **latvian** | **lithuanian_azerty** |
| **lithuanian_qwerty** | **norman** | **norwegian** | **portuguese** |
| **portuguese_osx_iso** | **romanian** | **serbian_latin** | **slovak** |
| **slovenian** | **spanish_dvorak** | **spanish** | **swedish** |
| **turkish_f** | **turkish_q** | **uk** | **us_international** |
| **workman** | **workman_zxcvm** |
### Macro Basics
Each macro is an array consisting of strings and objects (dictionaries.) Strings are typed to your computer while objects allow you to control how your macro is typed out.
#### Object Format
All objects have one required key: `action`. This tells QMK what the object does. There are currently 5 actions: beep, delay, down, tap, up
Only basic keycodes (prefixed by `KC_`) are supported. Do not include the `KC_` prefix when listing keycodes.
* `beep`
* Play a bell if the keyboard has [audio enabled](feature_audio.md).
* Example: `{"action": "beep"}`
* `delay`
* Pause macro playback. Duration is specified in milliseconds (ms).
* Example: `{"action": "delay", "duration": 500}`
* `down`
* Send a key down event for one or more keycodes.
* Example, single key: `{"action":"down", "keycodes": ["LSFT"]}`
* Example, multiple keys: `{"action":"down", "keycodes": ["CTRL", "LSFT"]}`
* `tap`
* Type a chord, which sends a down event for each key followed by an up event for each key.
* Example, single key: `{"action":"tap", "keycodes": ["F13"]}`
* Example, multiple keys: `{"action":"tap", "keycodes": ["CTRL", "LALT", "DEL"]}`
* `up`
* Send a key up event for one or more keycodes.
* Example, single key: `{"action":"up", "keycodes": ["LSFT"]}`
* Example, multiple keys: `{"action":"up", "keycodes": ["CTRL", "LSFT"]}`
## Using Macros in C Keymaps
### `SEND_STRING()` & `process_record_user`
Sometimes you want a key to type out words or phrases. For the most common situations, we've provided `SEND_STRING()`, which will type out a string (i.e. a sequence of characters) for you. All ASCII characters that are easily translatable to a keycode are supported (e.g. `qmk 123\n\t`).
@ -91,7 +191,7 @@ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
};
```
### Advanced Macros
#### Advanced Macros
In addition to the `process_record_user()` function, is the `post_process_record_user()` function. This runs after `process_record` and can be used to do things after a keystroke has been sent. This is useful if you want to have a key pressed before and released after a normal key, for instance.
@ -131,7 +231,7 @@ void post_process_record_user(uint16_t keycode, keyrecord_t *record) {
```
### TAP, DOWN and UP
#### TAP, DOWN and UP
You may want to use keys in your macros that you can't write down, such as `Ctrl` or `Home`.
You can send arbitrary keycodes by wrapping them in:
@ -178,7 +278,7 @@ They can be used like this:
Which would send Left Control+`a` (Left Control down, `a`, Left Control up) - notice that they take strings (eg `"k"`), and not the `X_K` keycodes.
### Alternative Keymaps
#### Alternative Keymaps
By default, it assumes a US keymap with a QWERTY layout; if you want to change that (e.g. if your OS uses software Colemak), include this somewhere in your keymap:
@ -186,7 +286,7 @@ By default, it assumes a US keymap with a QWERTY layout; if you want to change t
#include "sendstring_colemak.h"
```
### Strings in Memory
#### Strings in Memory
If for some reason you're manipulating strings and need to print out something you just generated (instead of being a literal, constant string), you can use `send_string()`, like this:
@ -205,13 +305,13 @@ SEND_STRING(".."SS_TAP(X_END));
```
## Advanced Macro Functions
### Advanced Macro Functions
There are some functions you may find useful in macro-writing. Keep in mind that while you can write some fairly advanced code within a macro, if your functionality gets too complex you may want to define a custom keycode instead. Macros are meant to be simple.
?> You can also use the functions described in [Useful function](ref_functions.md) and [Checking modifier state](feature_advanced_keycodes#checking-modifier-state) for additional functionality. For example, `reset_keyboard()` allows you to reset the keyboard as part of a macro and `get_mods() & MOD_MASK_SHIFT` lets you check for the existence of active shift modifiers.
### `record->event.pressed`
#### `record->event.pressed`
This is a boolean value that can be tested to see if the switch is being pressed or released. An example of this is
@ -223,15 +323,15 @@ This is a boolean value that can be tested to see if the switch is being pressed
}
```
### `register_code(<kc>);`
#### `register_code(<kc>);`
This sends the `<kc>` keydown event to the computer. Some examples would be `KC_ESC`, `KC_C`, `KC_4`, and even modifiers such as `KC_LSFT` and `KC_LGUI`.
### `unregister_code(<kc>);`
#### `unregister_code(<kc>);`
Parallel to `register_code` function, this sends the `<kc>` keyup event to the computer. If you don't use this, the key will be held down until it's sent.
### `tap_code(<kc>);`
#### `tap_code(<kc>);`
Sends `register_code(<kc>)` and then `unregister_code(<kc>)`. This is useful if you want to send both the press and release events ("tap" the key, rather than hold it).
@ -239,31 +339,31 @@ If `TAP_CODE_DELAY` is defined (default 0), this function waits that many millis
If the keycode is `KC_CAPS`, it waits `TAP_HOLD_CAPS_DELAY` milliseconds instead (default 80), as macOS prevents accidental Caps Lock activation by waiting for the key to be held for a certain amount of time.
### `tap_code_delay(<kc>, <delay>);`
#### `tap_code_delay(<kc>, <delay>);`
Like `tap_code(<kc>)`, but with a `delay` parameter for specifying arbitrary intervals before sending the unregister event.
### `register_code16(<kc>);`, `unregister_code16(<kc>);` and `tap_code16(<kc>);`
#### `register_code16(<kc>);`, `unregister_code16(<kc>);` and `tap_code16(<kc>);`
These functions work similar to their regular counterparts, but allow you to use modded keycodes (with Shift, Alt, Control, and/or GUI applied to them).
Eg, you could use `register_code16(S(KC_5));` instead of registering the mod, then registering the keycode.
### `clear_keyboard();`
#### `clear_keyboard();`
This will clear all mods and keys currently pressed.
### `clear_mods();`
#### `clear_mods();`
This will clear all mods currently pressed.
### `clear_keyboard_but_mods();`
#### `clear_keyboard_but_mods();`
This will clear all keys besides the mods currently pressed.
## Advanced Example:
### Advanced Example:
### Super ALT↯TAB
#### Super ALT↯TAB
This macro will register `KC_LALT` and tap `KC_TAB`, then wait for 1000ms. If the key is tapped again, it will send another `KC_TAB`; if there is no tap, `KC_LALT` will be unregistered, thus allowing you to cycle through windows.

0
keyboards/handwired/pytest/macro/.noci

10
keyboards/handwired/pytest/macro/info.json

@ -0,0 +1,10 @@
{
"maintainer": "qmk",
"layouts": {
"LAYOUT_custom": {
"layout": [
{ "label": "KC_Q", "matrix": [0, 0], "w": 1, "x": 0, "y": 0 }
]
}
}
}

15
keyboards/handwired/pytest/macro/keymaps/default/keymap.json

@ -0,0 +1,15 @@
{
"keyboard": "handwired/pytest/basic",
"keymap": "default_json",
"layout": "LAYOUT_ortho_1x1",
"layers": [["MACRO_0"]],
"macros": [
[
"Hello, World!",
{"action":"tap", "keycodes":["ENTER"]}
]
],
"author": "qmk",
"notes": "This file is a keymap.json file for handwired/pytest/basic",
"version": 1
}

0
keyboards/handwired/pytest/macro/readme.md

1
keyboards/handwired/pytest/macro/rules.mk

@ -0,0 +1 @@
MCU = atmega32u4

2
lib/python/qmk/cli/json2c.py

@ -33,7 +33,7 @@ def json2c(cli):
cli.args.output = None
# Generate the keymap
keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
keymap_c = qmk.keymap.generate_c(user_keymap)
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)

2
lib/python/qmk/commands.py

@ -190,7 +190,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
c_text = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
c_text = qmk.keymap.generate_c(user_keymap)
keymap_dir = keymap_output / 'src'
keymap_c = keymap_dir / 'keymap.c'

100
lib/python/qmk/keymap.py

@ -17,6 +17,7 @@ from qmk.errors import CppError
# The `keymap.c` template to use when a keyboard doesn't have its own
DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
__INCLUDES__
/* THIS FILE WAS GENERATED!
*
@ -27,6 +28,7 @@ DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
__KEYMAP_GOES_HERE__
};
"""
@ -180,10 +182,11 @@ def generate_json(keymap, keyboard, layout, layers):
return new_keymap
def generate_c(keyboard, layout, layers):
"""Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
def generate_c(keymap_json):
"""Returns a `keymap.c`.
`keymap_json` is a dictionary with the following keys:
Args:
keyboard
The name of the keyboard
@ -192,19 +195,89 @@ def generate_c(keyboard, layout, layers):
layers
An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
macros
A sequence of strings containing macros to implement for this keyboard.
"""
new_keymap = template_c(keyboard)
new_keymap = template_c(keymap_json['keyboard'])
layer_txt = []
for layer_num, layer in enumerate(layers):
for layer_num, layer in enumerate(keymap_json['layers']):
if layer_num != 0:
layer_txt[-1] = layer_txt[-1] + ','
layer = map(_strip_any, layer)
layer_keys = ', '.join(layer)
layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
layer_txt.append('\t[%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys))
keymap = '\n'.join(layer_txt)
new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
if keymap_json.get('macros'):
macro_txt = [
'bool process_record_user(uint16_t keycode, keyrecord_t *record) {',
' if (record->event.pressed) {',
' switch (keycode) {',
]
for i, macro_array in enumerate(keymap_json['macros']):
macro = []
for macro_fragment in macro_array:
if isinstance(macro_fragment, str):
macro_fragment = macro_fragment.replace('\\', '\\\\')
macro_fragment = macro_fragment.replace('\r\n', r'\n')
macro_fragment = macro_fragment.replace('\n', r'\n')
macro_fragment = macro_fragment.replace('\r', r'\n')
macro_fragment = macro_fragment.replace('\t', r'\t')
macro_fragment = macro_fragment.replace('"', r'\"')
macro.append(f'"{macro_fragment}"')
elif isinstance(macro_fragment, dict):
newstring = []
if macro_fragment['action'] == 'delay':
newstring.append(f"SS_DELAY({macro_fragment['duration']})")
elif macro_fragment['action'] == 'beep':
newstring.append(r'"\a"')
elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1:
last_keycode = macro_fragment['keycodes'].pop()
for keycode in macro_fragment['keycodes']:
newstring.append(f'SS_DOWN(X_{keycode})')
newstring.append(f'SS_TAP(X_{last_keycode})')
for keycode in reversed(macro_fragment['keycodes']):
newstring.append(f'SS_UP(X_{keycode})')
else:
for keycode in macro_fragment['keycodes']:
newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})")
macro.append(''.join(newstring))
new_macro = "".join(macro)
new_macro = new_macro.replace('""', '')
macro_txt.append(f' case MACRO_{i}:')
macro_txt.append(f' SEND_STRING({new_macro});')
macro_txt.append(' return false;')
macro_txt.append(' }')
macro_txt.append(' }')
macro_txt.append('\n return true;')
macro_txt.append('};')
macro_txt.append('')
new_keymap = '\n'.join((new_keymap, *macro_txt))
if keymap_json.get('host_language'):
new_keymap = new_keymap.replace('__INCLUDES__', f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n')
else:
new_keymap = new_keymap.replace('__INCLUDES__', '')
return new_keymap
@ -217,7 +290,7 @@ def write_file(keymap_filename, keymap_content):
return keymap_filename
def write_json(keyboard, keymap, layout, layers):
def write_json(keyboard, keymap, layout, layers, macros=None):
"""Generate the `keymap.json` and write it to disk.
Returns the filename written to.
@ -235,19 +308,19 @@ def write_json(keyboard, keymap, layout, layers):
layers
An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
"""
keymap_json = generate_json(keyboard, keymap, layout, layers)
keymap_json = generate_json(keyboard, keymap, layout, layers, macros=None)
keymap_content = json.dumps(keymap_json)
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
return write_file(keymap_file, keymap_content)
def write(keyboard, keymap, layout, layers):
def write(keymap_json):
"""Generate the `keymap.c` and write it to disk.
Returns the filename written to.
Args:
`keymap_json` should be a dict with the following keys:
keyboard
The name of the keyboard
@ -259,9 +332,12 @@ def write(keyboard, keymap, layout, layers):
layers
An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
macros
A list of macros for this keymap.
"""
keymap_content = generate_c(keyboard, layout, layers)
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
keymap_content = generate_c(keymap_json)
keymap_file = qmk.path.keymap(keymap_json['keyboard']) / keymap_json['keymap'] / 'keymap.c'
return write_file(keymap_file, keymap_content)

8
lib/python/qmk/tests/test_cli_commands.py

@ -142,6 +142,14 @@ def test_json2c():
assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT_ortho_1x1(KC_A)};\n\n'
def test_json2c_macros():
result = check_subcommand("json2c", 'keyboards/handwired/pytest/macro/keymaps/default/keymap.json')
check_returncode(result)
assert 'LAYOUT_ortho_1x1(MACRO_0)' in result.stdout
assert 'case MACRO_0:' in result.stdout
assert 'SEND_STRING("Hello, World!"SS_TAP(X_ENTER));' in result.stdout
def test_json2c_stdin():
result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-')
check_returncode(result)

8
lib/python/qmk/tests/test_qmk_keymap.py

@ -22,7 +22,13 @@ def test_template_json_pytest_has_template():
def test_generate_c_pytest_has_template():
templ = qmk.keymap.generate_c('handwired/pytest/has_template', 'LAYOUT', [['KC_A']])
keymap_json = {
'keyboard': 'handwired/pytest/has_template',
'layout': 'LAYOUT',
'layers': [['KC_A']],
'macros': None,
}
templ = qmk.keymap.generate_c(keymap_json)
assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n'

34
quantum/quantum_keycodes.h

@ -558,6 +558,40 @@ enum quantum_keycodes {
PROGRAMMABLE_BUTTON_31,
PROGRAMMABLE_BUTTON_32,
// Dedicated macro keys for Configurator and VIA
MACRO_0,
MACRO_1,
MACRO_2,
MACRO_3,
MACRO_4,
MACRO_5,
MACRO_6,
MACRO_7,
MACRO_8,
MACRO_9,
MACRO_10,
MACRO_11,
MACRO_12,
MACRO_13,
MACRO_14,
MACRO_15,
MACRO_16,
MACRO_17,
MACRO_18,
MACRO_19,
MACRO_20,
MACRO_21,
MACRO_22,
MACRO_23,
MACRO_24,
MACRO_25,
MACRO_26,
MACRO_27,
MACRO_28,
MACRO_29,
MACRO_30,
MACRO_31,
// Start of custom keycode range for keyboards and keymaps - always leave at the end
SAFE_RANGE
};

1
requirements-dev.txt

@ -5,4 +5,5 @@
nose2
flake8
pep8-naming
pyflakes
yapf
Loading…
Cancel
Save