diff options
| author | Zach White <skullydazed@gmail.com> | 2021-11-22 11:11:35 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-11-22 11:11:35 -0800 |
| commit | 08ce0142bad40f22d05d33fdef8a7c8907154e96 (patch) | |
| tree | 5b5da4650a76ec902a550e2719b79ffc2a73d74d /lib | |
| parent | 8181b155dbfd07561200b30b52a4046f2da92248 (diff) | |
| download | qmk_firmware-08ce0142bad40f22d05d33fdef8a7c8907154e96.tar.gz qmk_firmware-08ce0142bad40f22d05d33fdef8a7c8907154e96.zip | |
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>
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/python/qmk/cli/json2c.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/commands.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/keymap.py | 100 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 8 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_qmk_keymap.py | 8 |
5 files changed, 105 insertions, 15 deletions
diff --git a/lib/python/qmk/cli/json2c.py b/lib/python/qmk/cli/json2c.py index a90578c02..ae8248e6b 100755 --- a/lib/python/qmk/cli/json2c.py +++ b/lib/python/qmk/cli/json2c.py | |||
| @@ -33,7 +33,7 @@ def json2c(cli): | |||
| 33 | cli.args.output = None | 33 | cli.args.output = None |
| 34 | 34 | ||
| 35 | # Generate the keymap | 35 | # Generate the keymap |
| 36 | keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers']) | 36 | keymap_c = qmk.keymap.generate_c(user_keymap) |
| 37 | 37 | ||
| 38 | if cli.args.output: | 38 | if cli.args.output: |
| 39 | cli.args.output.parent.mkdir(parents=True, exist_ok=True) | 39 | cli.args.output.parent.mkdir(parents=True, exist_ok=True) |
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 2995a5fda..5a0194377 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py | |||
| @@ -190,7 +190,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va | |||
| 190 | target = f'{keyboard_filesafe}_{user_keymap["keymap"]}' | 190 | target = f'{keyboard_filesafe}_{user_keymap["keymap"]}' |
| 191 | keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}') | 191 | keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}') |
| 192 | keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}') | 192 | keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}') |
| 193 | c_text = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers']) | 193 | c_text = qmk.keymap.generate_c(user_keymap) |
| 194 | keymap_dir = keymap_output / 'src' | 194 | keymap_dir = keymap_output / 'src' |
| 195 | keymap_c = keymap_dir / 'keymap.c' | 195 | keymap_c = keymap_dir / 'keymap.c' |
| 196 | 196 | ||
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 6eec49cfd..00b5a78a5 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py | |||
| @@ -17,6 +17,7 @@ from qmk.errors import CppError | |||
| 17 | 17 | ||
| 18 | # The `keymap.c` template to use when a keyboard doesn't have its own | 18 | # The `keymap.c` template to use when a keyboard doesn't have its own |
| 19 | DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H | 19 | DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H |
| 20 | __INCLUDES__ | ||
| 20 | 21 | ||
| 21 | /* THIS FILE WAS GENERATED! | 22 | /* THIS FILE WAS GENERATED! |
| 22 | * | 23 | * |
| @@ -27,6 +28,7 @@ DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H | |||
| 27 | const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { | 28 | const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { |
| 28 | __KEYMAP_GOES_HERE__ | 29 | __KEYMAP_GOES_HERE__ |
| 29 | }; | 30 | }; |
| 31 | |||
| 30 | """ | 32 | """ |
| 31 | 33 | ||
| 32 | 34 | ||
| @@ -180,10 +182,11 @@ def generate_json(keymap, keyboard, layout, layers): | |||
| 180 | return new_keymap | 182 | return new_keymap |
| 181 | 183 | ||
| 182 | 184 | ||
| 183 | def generate_c(keyboard, layout, layers): | 185 | def generate_c(keymap_json): |
| 184 | """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers. | 186 | """Returns a `keymap.c`. |
| 187 | |||
| 188 | `keymap_json` is a dictionary with the following keys: | ||
| 185 | 189 | ||
| 186 | Args: | ||
| 187 | keyboard | 190 | keyboard |
| 188 | The name of the keyboard | 191 | The name of the keyboard |
| 189 | 192 | ||
| @@ -192,19 +195,89 @@ def generate_c(keyboard, layout, layers): | |||
| 192 | 195 | ||
| 193 | layers | 196 | layers |
| 194 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. | 197 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. |
| 198 | |||
| 199 | macros | ||
| 200 | A sequence of strings containing macros to implement for this keyboard. | ||
| 195 | """ | 201 | """ |
| 196 | new_keymap = template_c(keyboard) | 202 | new_keymap = template_c(keymap_json['keyboard']) |
| 197 | layer_txt = [] | 203 | layer_txt = [] |
| 198 | for layer_num, layer in enumerate(layers): | 204 | |
| 205 | for layer_num, layer in enumerate(keymap_json['layers']): | ||
| 199 | if layer_num != 0: | 206 | if layer_num != 0: |
| 200 | layer_txt[-1] = layer_txt[-1] + ',' | 207 | layer_txt[-1] = layer_txt[-1] + ',' |
| 201 | layer = map(_strip_any, layer) | 208 | layer = map(_strip_any, layer) |
| 202 | layer_keys = ', '.join(layer) | 209 | layer_keys = ', '.join(layer) |
| 203 | layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys)) | 210 | layer_txt.append('\t[%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys)) |
| 204 | 211 | ||
| 205 | keymap = '\n'.join(layer_txt) | 212 | keymap = '\n'.join(layer_txt) |
| 206 | new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap) | 213 | new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap) |
| 207 | 214 | ||
| 215 | if keymap_json.get('macros'): | ||
| 216 | macro_txt = [ | ||
| 217 | 'bool process_record_user(uint16_t keycode, keyrecord_t *record) {', | ||
| 218 | ' if (record->event.pressed) {', | ||
| 219 | ' switch (keycode) {', | ||
| 220 | ] | ||
| 221 | |||
| 222 | for i, macro_array in enumerate(keymap_json['macros']): | ||
| 223 | macro = [] | ||
| 224 | |||
| 225 | for macro_fragment in macro_array: | ||
| 226 | if isinstance(macro_fragment, str): | ||
| 227 | macro_fragment = macro_fragment.replace('\\', '\\\\') | ||
| 228 | macro_fragment = macro_fragment.replace('\r\n', r'\n') | ||
| 229 | macro_fragment = macro_fragment.replace('\n', r'\n') | ||
| 230 | macro_fragment = macro_fragment.replace('\r', r'\n') | ||
| 231 | macro_fragment = macro_fragment.replace('\t', r'\t') | ||
| 232 | macro_fragment = macro_fragment.replace('"', r'\"') | ||
| 233 | |||
| 234 | macro.append(f'"{macro_fragment}"') | ||
| 235 | |||
| 236 | elif isinstance(macro_fragment, dict): | ||
| 237 | newstring = [] | ||
| 238 | |||
| 239 | if macro_fragment['action'] == 'delay': | ||
| 240 | newstring.append(f"SS_DELAY({macro_fragment['duration']})") | ||
| 241 | |||
| 242 | elif macro_fragment['action'] == 'beep': | ||
| 243 | newstring.append(r'"\a"') | ||
| 244 | |||
| 245 | elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1: | ||
| 246 | last_keycode = macro_fragment['keycodes'].pop() | ||
| 247 | |||
| 248 | for keycode in macro_fragment['keycodes']: | ||
| 249 | newstring.append(f'SS_DOWN(X_{keycode})') | ||
| 250 | |||
| 251 | newstring.append(f'SS_TAP(X_{last_keycode})') | ||
| 252 | |||
| 253 | for keycode in reversed(macro_fragment['keycodes']): | ||
| 254 | newstring.append(f'SS_UP(X_{keycode})') | ||
| 255 | |||
| 256 | else: | ||
| 257 | for keycode in macro_fragment['keycodes']: | ||
| 258 | newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})") | ||
| 259 | |||
| 260 | macro.append(''.join(newstring)) | ||
| 261 | |||
| 262 | new_macro = "".join(macro) | ||
| 263 | new_macro = new_macro.replace('""', '') | ||
| 264 | macro_txt.append(f' case MACRO_{i}:') | ||
| 265 | macro_txt.append(f' SEND_STRING({new_macro});') | ||
| 266 | macro_txt.append(' return false;') | ||
| 267 | |||
| 268 | macro_txt.append(' }') | ||
| 269 | macro_txt.append(' }') | ||
| 270 | macro_txt.append('\n return true;') | ||
| 271 | macro_txt.append('};') | ||
| 272 | macro_txt.append('') | ||
| 273 | |||
| 274 | new_keymap = '\n'.join((new_keymap, *macro_txt)) | ||
| 275 | |||
| 276 | if keymap_json.get('host_language'): | ||
| 277 | new_keymap = new_keymap.replace('__INCLUDES__', f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n') | ||
| 278 | else: | ||
| 279 | new_keymap = new_keymap.replace('__INCLUDES__', '') | ||
| 280 | |||
| 208 | return new_keymap | 281 | return new_keymap |
| 209 | 282 | ||
| 210 | 283 | ||
| @@ -217,7 +290,7 @@ def write_file(keymap_filename, keymap_content): | |||
| 217 | return keymap_filename | 290 | return keymap_filename |
| 218 | 291 | ||
| 219 | 292 | ||
| 220 | def write_json(keyboard, keymap, layout, layers): | 293 | def write_json(keyboard, keymap, layout, layers, macros=None): |
| 221 | """Generate the `keymap.json` and write it to disk. | 294 | """Generate the `keymap.json` and write it to disk. |
| 222 | 295 | ||
| 223 | Returns the filename written to. | 296 | Returns the filename written to. |
| @@ -235,19 +308,19 @@ def write_json(keyboard, keymap, layout, layers): | |||
| 235 | layers | 308 | layers |
| 236 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. | 309 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. |
| 237 | """ | 310 | """ |
| 238 | keymap_json = generate_json(keyboard, keymap, layout, layers) | 311 | keymap_json = generate_json(keyboard, keymap, layout, layers, macros=None) |
| 239 | keymap_content = json.dumps(keymap_json) | 312 | keymap_content = json.dumps(keymap_json) |
| 240 | keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json' | 313 | keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json' |
| 241 | 314 | ||
| 242 | return write_file(keymap_file, keymap_content) | 315 | return write_file(keymap_file, keymap_content) |
| 243 | 316 | ||
| 244 | 317 | ||
| 245 | def write(keyboard, keymap, layout, layers): | 318 | def write(keymap_json): |
| 246 | """Generate the `keymap.c` and write it to disk. | 319 | """Generate the `keymap.c` and write it to disk. |
| 247 | 320 | ||
| 248 | Returns the filename written to. | 321 | Returns the filename written to. |
| 249 | 322 | ||
| 250 | Args: | 323 | `keymap_json` should be a dict with the following keys: |
| 251 | keyboard | 324 | keyboard |
| 252 | The name of the keyboard | 325 | The name of the keyboard |
| 253 | 326 | ||
| @@ -259,9 +332,12 @@ def write(keyboard, keymap, layout, layers): | |||
| 259 | 332 | ||
| 260 | layers | 333 | layers |
| 261 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. | 334 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. |
| 335 | |||
| 336 | macros | ||
| 337 | A list of macros for this keymap. | ||
| 262 | """ | 338 | """ |
| 263 | keymap_content = generate_c(keyboard, layout, layers) | 339 | keymap_content = generate_c(keymap_json) |
| 264 | keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c' | 340 | keymap_file = qmk.path.keymap(keymap_json['keyboard']) / keymap_json['keymap'] / 'keymap.c' |
| 265 | 341 | ||
| 266 | return write_file(keymap_file, keymap_content) | 342 | return write_file(keymap_file, keymap_content) |
| 267 | 343 | ||
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index 1e3c64e73..2973f8170 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py | |||
| @@ -142,6 +142,14 @@ def test_json2c(): | |||
| 142 | 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' | 142 | 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' |
| 143 | 143 | ||
| 144 | 144 | ||
| 145 | def test_json2c_macros(): | ||
| 146 | result = check_subcommand("json2c", 'keyboards/handwired/pytest/macro/keymaps/default/keymap.json') | ||
| 147 | check_returncode(result) | ||
| 148 | assert 'LAYOUT_ortho_1x1(MACRO_0)' in result.stdout | ||
| 149 | assert 'case MACRO_0:' in result.stdout | ||
| 150 | assert 'SEND_STRING("Hello, World!"SS_TAP(X_ENTER));' in result.stdout | ||
| 151 | |||
| 152 | |||
| 145 | def test_json2c_stdin(): | 153 | def test_json2c_stdin(): |
| 146 | result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-') | 154 | result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-') |
| 147 | check_returncode(result) | 155 | check_returncode(result) |
diff --git a/lib/python/qmk/tests/test_qmk_keymap.py b/lib/python/qmk/tests/test_qmk_keymap.py index b9e80df67..5e2efc123 100644 --- a/lib/python/qmk/tests/test_qmk_keymap.py +++ b/lib/python/qmk/tests/test_qmk_keymap.py | |||
| @@ -22,7 +22,13 @@ def test_template_json_pytest_has_template(): | |||
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | def test_generate_c_pytest_has_template(): | 24 | def test_generate_c_pytest_has_template(): |
| 25 | templ = qmk.keymap.generate_c('handwired/pytest/has_template', 'LAYOUT', [['KC_A']]) | 25 | keymap_json = { |
| 26 | 'keyboard': 'handwired/pytest/has_template', | ||
| 27 | 'layout': 'LAYOUT', | ||
| 28 | 'layers': [['KC_A']], | ||
| 29 | 'macros': None, | ||
| 30 | } | ||
| 31 | templ = qmk.keymap.generate_c(keymap_json) | ||
| 26 | assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n' | 32 | assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n' |
| 27 | 33 | ||
| 28 | 34 | ||
