08ce0142bad40f22d05d33fdef8a7c8907154e96

Author
Zach White <skullydazed@gmail.com>
Committer
GitHub <noreply@github.com>
Date

Message

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>

Diff

This diff is truncated to protect this page.

  1diff --git a/data/mappings/info_config.json b/data/mappings/info_config.json
  2index 50694e38632dbd5b4a3a817ff7ce09d36965e5ec..3cb64dd3a2fbe968de16c44daa31366398d615e4 100644
  3--- a/data/mappings/info_config.json
  4+++ b/data/mappings/info_config.json
  5@@ -76,6 +76,7 @@
  6     "QMK_KEYS_PER_SCAN": {"info_key": "qmk.keys_per_scan", "value_type": "int"},
  7     "QMK_LED": {"info_key": "qmk_lufa_bootloader.led"},
  8     "QMK_SPEAKER": {"info_key": "qmk_lufa_bootloader.speaker"},
  9+    "SENDSTRING_BELL": {"info_key": "audio.macro_beep", "value_type": "bool"},
 10     "SPLIT_MODS_ENABLE": {"info_key": "split.transport.sync_modifiers", "value_type": "bool"},
 11     "SPLIT_TRANSPORT_MIRROR": {"info_key": "split.transport.sync_matrix_state", "value_type": "bool"},
 12     "SPLIT_USB_DETECT": {"info_key": "split.usb_detect.enabled", "value_type": "bool"},
 13diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
 14index 65d44c94d2ce8d2ad279cc3dccafac3e3db3f179..37a0643ab2e89207964d2d10900764830114877b 100644
 15--- a/data/schemas/keyboard.jsonschema
 16+++ b/data/schemas/keyboard.jsonschema
 17@@ -19,6 +19,7 @@
 18             "type": "object",
 19             "additionalProperties": false,
 20             "properties": {
 21+                "macro_beep": {"type": "boolean"},
 22                 "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"},
 23                 "voices": {"type": "boolean"}
 24             }
 25diff --git a/data/schemas/keymap.jsonschema b/data/schemas/keymap.jsonschema
 26index a4bdab966b6149f5b1c00188df8d5415762db2eb..faa250a9429ca420f014efaac6c299cd693465b0 100644
 27--- a/data/schemas/keymap.jsonschema
 28+++ b/data/schemas/keymap.jsonschema
 29@@ -5,6 +5,7 @@
 30     "type": "object",
 31     "properties": {
 32         "author": {"type": "string"},
 33+        "host_language": {"$ref": "qmk.definitions.v1#/text_identifier"},
 34         "keyboard": {"$ref": "qmk.definitions.v1#/text_identifier"},
 35         "keymap": {"$ref": "qmk.definitions.v1#/text_identifier"},
 36         "layout": {"$ref": "qmk.definitions.v1#/layout_macro"},
 37@@ -15,10 +16,42 @@
 38                 "items": {"type": "string"}
 39             }
 40         },
 41+        "macros": {
 42+            "type": "array",
 43+            "items": {
 44+                "type": "array",
 45+                "items": {
 46+                    "oneOf": [
 47+                        {
 48+                            "type": "string"
 49+                        },
 50+                        {
 51+                            "type": "object",
 52+                            "additionalProperties": false,
 53+                            "properties": {
 54+                                "action": {
 55+                                    "type": "string",
 56+                                    "enum": ['beep', 'delay', 'down', 'tap', 'up']
 57+                                },
 58+                                "keycodes": {
 59+                                    "type": "array",
 60+                                    "items": {
 61+                                        "$ref": "qmk.definitions.v1#/text_identifier"
 62+                                    }
 63+                                },
 64+                                "duration": {
 65+                                    "$ref": "qmk.definitions.v1#/unsigned_int"
 66+                                }
 67+                            }
 68+                        }
 69+                    ]
 70+                }
 71+            }
 72+        },
 73         "config": {"$ref": "qmk.keyboard.v1"},
 74         "notes": {
 75             "type": "string",
 76             "description": "asdf"
 77         }
 78     }
 79-}
 80+}
 81diff --git a/docs/feature_macros.md b/docs/feature_macros.md
 82index 6807111ca2d38d8896f5978036ce94e2a26118cd..81ade5859261a45c259905dd6df276b4a591eca6 100644
 83--- a/docs/feature_macros.md
 84+++ b/docs/feature_macros.md
 85@@ -4,7 +4,107 @@ Macros allow you to send multiple keystrokes when pressing just one key. QMK has
 86 
 87diff --git a/keyboards/handwired/pytest/macro/.noci b/keyboards/handwired/pytest/macro/.noci
 88new file mode 100644
 89index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
 90--- /dev/null
 91+++ b/keyboards/handwired/pytest/macro/.noci
 92diff --git a/keyboards/handwired/pytest/macro/info.json b/keyboards/handwired/pytest/macro/info.json
 93new file mode 100644
 94index 0000000000000000000000000000000000000000..ed052a14a3ee8d0e1208202993781ca925cd8628
 95--- /dev/null
 96+++ b/keyboards/handwired/pytest/macro/info.json
 97@@ -0,0 +1,10 @@
 98+{
 99+  "maintainer": "qmk",
100+  "layouts": {
101+    "LAYOUT_custom": {
102+      "layout": [
103+        { "label": "KC_Q", "matrix": [0, 0], "w": 1, "x": 0, "y": 0 }
104+      ]
105+    }
106+  }
107+}
108diff --git a/keyboards/handwired/pytest/macro/keymaps/default/keymap.json b/keyboards/handwired/pytest/macro/keymaps/default/keymap.json
109new file mode 100644
110index 0000000000000000000000000000000000000000..f319d862d83335f543d399b380a2abae19cda81f
111--- /dev/null
112+++ b/keyboards/handwired/pytest/macro/keymaps/default/keymap.json
113@@ -0,0 +1,15 @@
114+{
115+    "keyboard": "handwired/pytest/basic",
116+    "keymap": "default_json",
117+    "layout": "LAYOUT_ortho_1x1",
118+    "layers": [["MACRO_0"]],
119+    "macros": [
120+        [
121+            "Hello, World!",
122+            {"action":"tap", "keycodes":["ENTER"]}
123+	]
124+    ],
125+    "author": "qmk",
126+    "notes": "This file is a keymap.json file for handwired/pytest/basic",
127+    "version": 1
128+}
129diff --git a/keyboards/handwired/pytest/macro/readme.md b/keyboards/handwired/pytest/macro/readme.md
130new file mode 100644
131index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
132--- /dev/null
133+++ b/keyboards/handwired/pytest/macro/readme.md
134diff --git a/keyboards/handwired/pytest/macro/rules.mk b/keyboards/handwired/pytest/macro/rules.mk
135new file mode 100644
136index 0000000000000000000000000000000000000000..6b42774dbffb0b5faeb45a5d39a2d40c1e26d1a3
137--- /dev/null
138+++ b/keyboards/handwired/pytest/macro/rules.mk
139@@ -0,0 +1 @@
140+MCU = atmega32u4
141diff --git a/lib/python/qmk/cli/json2c.py b/lib/python/qmk/cli/json2c.py
142index a90578c021d791eb77c8f310b4ca78c8c1e130bb..ae8248e6b72a04fcadcc183b88ddb2861acccd22 100755
143--- a/lib/python/qmk/cli/json2c.py
144+++ b/lib/python/qmk/cli/json2c.py
145@@ -33,7 +33,7 @@ def json2c(cli):
146         cli.args.output = None
147 
148     # Generate the keymap
149-    keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
150+    keymap_c = qmk.keymap.generate_c(user_keymap)
151 
152     if cli.args.output:
153         cli.args.output.parent.mkdir(parents=True, exist_ok=True)
154diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
155index 2995a5fdabebd75d3131a35665d8ba537f3f257d..5a019437734c398bf0ba383a049c35ffaaca9ca0 100644
156--- a/lib/python/qmk/commands.py
157+++ b/lib/python/qmk/commands.py
158@@ -190,7 +190,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
159     target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
160     keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
161     keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
162-    c_text = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
163+    c_text = qmk.keymap.generate_c(user_keymap)
164     keymap_dir = keymap_output / 'src'
165     keymap_c = keymap_dir / 'keymap.c'
166 
167diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
168index 6eec49cfd1a7793529632d0ad24a1ff1434ab69d..00b5a78a5ac510bc0384c08c3936014261bb63f5 100644
169--- a/lib/python/qmk/keymap.py
170+++ b/lib/python/qmk/keymap.py
171@@ -17,6 +17,7 @@ from qmk.errors import CppError
172 
173 # The `keymap.c` template to use when a keyboard doesn't have its own
174 DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
175+__INCLUDES__
176 
177 /* THIS FILE WAS GENERATED!
178  *
179@@ -27,6 +28,7 @@ DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
180 const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
181 __KEYMAP_GOES_HERE__
182 };
183+
184 """
185 
186 
187@@ -180,10 +182,11 @@ def generate_json(keymap, keyboard, layout, layers):
188     return new_keymap
189 
190 
191-def generate_c(keyboard, layout, layers):
192-    """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
193+def generate_c(keymap_json):
194+    """Returns a `keymap.c`.
195+
196+    `keymap_json` is a dictionary with the following keys:
197 
198-    Args:
199         keyboard
200             The name of the keyboard
201 
202@@ -192,19 +195,89 @@ def generate_c(keyboard, layout, layers):
203 
204         layers
205             An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
206+
207+        macros
208+            A sequence of strings containing macros to implement for this keyboard.
209     """
210-    new_keymap = template_c(keyboard)
211+    new_keymap = template_c(keymap_json['keyboard'])
212     layer_txt = []
213-    for layer_num, layer in enumerate(layers):
214+
215+    for layer_num, layer in enumerate(keymap_json['layers']):
216         if layer_num != 0:
217             layer_txt[-1] = layer_txt[-1] + ','
218         layer = map(_strip_any, layer)
219         layer_keys = ', '.join(layer)
220-        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
221+        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys))
222 
223     keymap = '\n'.join(layer_txt)
224     new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
225 
226+    if keymap_json.get('macros'):
227+        macro_txt = [
228+            'bool process_record_user(uint16_t keycode, keyrecord_t *record) {',
229+            '    if (record->event.pressed) {',
230+            '        switch (keycode) {',
231+        ]
232+
233+        for i, macro_array in enumerate(keymap_json['macros']):
234+            macro = []
235+
236+            for macro_fragment in macro_array:
237+                if isinstance(macro_fragment, str):
238+                    macro_fragment = macro_fragment.replace('\\', '\\\\')
239+                    macro_fragment = macro_fragment.replace('\r\n', r'\n')
240+                    macro_fragment = macro_fragment.replace('\n', r'\n')
241+                    macro_fragment = macro_fragment.replace('\r', r'\n')
242+                    macro_fragment = macro_fragment.replace('\t', r'\t')
243+                    macro_fragment = macro_fragment.replace('"', r'\"')
244+
245+                    macro.append(f'"{macro_fragment}"')
246+
247+                elif isinstance(macro_fragment, dict):
248+                    newstring = []
249+
250+                    if macro_fragment['action'] == 'delay':
251+                        newstring.append(f"SS_DELAY({macro_fragment['duration']})")
252+
253+                    elif macro_fragment['action'] == 'beep':
254+                        newstring.append(r'"\a"')
255+
256+                    elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1:
257+                        last_keycode = macro_fragment['keycodes'].pop()
258+
259+                        for keycode in macro_fragment['keycodes']:
260+                            newstring.append(f'SS_DOWN(X_{keycode})')
261+
262+                        newstring.append(f'SS_TAP(X_{last_keycode})')
263+
264+                        for keycode in reversed(macro_fragment['keycodes']):
265+                            newstring.append(f'SS_UP(X_{keycode})')
266+
267+                    else:
268+                        for keycode in macro_fragment['keycodes']:
269+                            newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})")
270+
271diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
272index 1e3c64e7352656d80aadb3920c4e83390cdd0686..2973f817022e6a481f4d7faddc173040672d01d3 100644
273--- a/lib/python/qmk/tests/test_cli_commands.py
274+++ b/lib/python/qmk/tests/test_cli_commands.py
275@@ -142,6 +142,14 @@ def test_json2c():
276     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'
277 
278 
279+def test_json2c_macros():
280+    result = check_subcommand("json2c", 'keyboards/handwired/pytest/macro/keymaps/default/keymap.json')
281+    check_returncode(result)
282+    assert 'LAYOUT_ortho_1x1(MACRO_0)' in result.stdout
283+    assert 'case MACRO_0:' in result.stdout
284+    assert 'SEND_STRING("Hello, World!"SS_TAP(X_ENTER));' in result.stdout
285+
286+
287 def test_json2c_stdin():
288     result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-')
289     check_returncode(result)
290diff --git a/lib/python/qmk/tests/test_qmk_keymap.py b/lib/python/qmk/tests/test_qmk_keymap.py
291index b9e80df6724996b07dff0a031214054d9896cb7d..5e2efc12329b9948b494ee3021dbecacd089e8e6 100644
292--- a/lib/python/qmk/tests/test_qmk_keymap.py
293+++ b/lib/python/qmk/tests/test_qmk_keymap.py
294@@ -22,7 +22,13 @@ def test_template_json_pytest_has_template():
295 
296 
297 def test_generate_c_pytest_has_template():
298-    templ = qmk.keymap.generate_c('handwired/pytest/has_template', 'LAYOUT', [['KC_A']])
299+    keymap_json = {
300+        'keyboard': 'handwired/pytest/has_template',
301+        'layout': 'LAYOUT',
302+        'layers': [['KC_A']],
303+        'macros': None,
304+    }
305+    templ = qmk.keymap.generate_c(keymap_json)
306     assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n'
307 
308 
309diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h
310index cde97074d398134837978918ec85a576ddca938c..d013a6a169d94a9947baec8cc39142c474f6a358 100644
311--- a/quantum/quantum_keycodes.h
312+++ b/quantum/quantum_keycodes.h
313@@ -558,6 +558,40 @@ enum quantum_keycodes {
314     PROGRAMMABLE_BUTTON_31,
315     PROGRAMMABLE_BUTTON_32,
316 
317+    // Dedicated macro keys for Configurator and VIA
318+    MACRO_0,
319+    MACRO_1,
320+    MACRO_2,
321+    MACRO_3,
322+    MACRO_4,
323+    MACRO_5,
324+    MACRO_6,
325+    MACRO_7,
326+    MACRO_8,
327+    MACRO_9,
328+    MACRO_10,
329+    MACRO_11,
330+    MACRO_12,
331+    MACRO_13,
332+    MACRO_14,
333+    MACRO_15,
334+    MACRO_16,
335+    MACRO_17,
336+    MACRO_18,
337+    MACRO_19,
338+    MACRO_20,
339+    MACRO_21,
340+    MACRO_22,
341+    MACRO_23,
342+    MACRO_24,
343+    MACRO_25,
344+    MACRO_26,
345+    MACRO_27,
346+    MACRO_28,
347+    MACRO_29,
348+    MACRO_30,
349+    MACRO_31,
350+
351     // Start of custom keycode range for keyboards and keymaps - always leave at the end
352     SAFE_RANGE
353 };
354diff --git a/requirements-dev.txt b/requirements-dev.txt
355index 1db3b6d7331545d253b7cd8b27cd17feacef665d..72137073381c04b8f63a43141b1dfd93145a925c 100644
356--- a/requirements-dev.txt
357+++ b/requirements-dev.txt
358@@ -5,4 +5,5 @@
359 nose2
360 flake8
361 pep8-naming
362+pyflakes
363 yapf