diff options
| author | Erovia <Erovia@users.noreply.github.com> | 2022-03-24 20:13:40 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-03-24 21:13:40 +0100 |
| commit | efc9c525b19b33c6e09057218ea64f07f45f9555 (patch) | |
| tree | 765e189c47e7e3a7bf8d18bbfbc45913546d07c4 | |
| parent | f7a5ec2483ef05d22b6604f0da1447cea5281243 (diff) | |
CLI: Add 'via2json' subcommand (#16468)
| -rw-r--r-- | docs/cli_commands.md | 17 | ||||
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/via2json.py | 145 | ||||
| -rwxr-xr-x | lib/python/qmk/json_encoders.py | 8 | ||||
| -rw-r--r-- | lib/python/qmk/keymap.py | 7 |
5 files changed, 176 insertions, 2 deletions
diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 93af906b8a..463abcef12 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md | |||
| @@ -335,6 +335,23 @@ This command cleans up the `.build` folder. If `--all` is passed, any .hex or .b | |||
| 335 | qmk clean [-a] | 335 | qmk clean [-a] |
| 336 | ``` | 336 | ``` |
| 337 | 337 | ||
| 338 | ## `qmk via2json` | ||
| 339 | |||
| 340 | This command an generate a keymap.json from a VIA keymap backup. Both the layers and the macros are converted, enabling users to easily move away from a VIA-enabled firmware without writing any code or reimplementing their keymaps in QMK Configurator. | ||
| 341 | |||
| 342 | **Usage**: | ||
| 343 | |||
| 344 | ``` | ||
| 345 | qmk via2json -kb KEYBOARD [-l LAYOUT] [-km KEYMAP] [-o OUTPUT] filename | ||
| 346 | ``` | ||
| 347 | |||
| 348 | **Example:** | ||
| 349 | |||
| 350 | ``` | ||
| 351 | $ qmk via2json -kb ai03/polaris -o polaris_keymap.json polaris_via_backup.json | ||
| 352 | Ψ Wrote keymap to /home/you/qmk_firmware/polaris_keymap.json | ||
| 353 | ``` | ||
| 354 | |||
| 338 | --- | 355 | --- |
| 339 | 356 | ||
| 340 | # Developer Commands | 357 | # Developer Commands |
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index c51eece955..5f65e677e5 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
| @@ -69,6 +69,7 @@ subcommands = [ | |||
| 69 | 'qmk.cli.new.keymap', | 69 | 'qmk.cli.new.keymap', |
| 70 | 'qmk.cli.pyformat', | 70 | 'qmk.cli.pyformat', |
| 71 | 'qmk.cli.pytest', | 71 | 'qmk.cli.pytest', |
| 72 | 'qmk.cli.via2json', | ||
| 72 | ] | 73 | ] |
| 73 | 74 | ||
| 74 | 75 | ||
diff --git a/lib/python/qmk/cli/via2json.py b/lib/python/qmk/cli/via2json.py new file mode 100755 index 0000000000..6edc9dfbe5 --- /dev/null +++ b/lib/python/qmk/cli/via2json.py | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | """Generate a keymap.c from a configurator export. | ||
| 2 | """ | ||
| 3 | import json | ||
| 4 | import re | ||
| 5 | |||
| 6 | from milc import cli | ||
| 7 | |||
| 8 | import qmk.keyboard | ||
| 9 | import qmk.path | ||
| 10 | from qmk.info import info_json | ||
| 11 | from qmk.json_encoders import KeymapJSONEncoder | ||
| 12 | from qmk.commands import parse_configurator_json, dump_lines | ||
| 13 | from qmk.keymap import generate_json, list_keymaps, locate_keymap, parse_keymap_c | ||
| 14 | |||
| 15 | |||
| 16 | def _find_via_layout_macro(keyboard): | ||
| 17 | keymap_layout = None | ||
| 18 | if 'via' in list_keymaps(keyboard): | ||
| 19 | keymap_path = locate_keymap(keyboard, 'via') | ||
| 20 | if keymap_path.suffix == '.json': | ||
| 21 | keymap_layout = parse_configurator_json(keymap_path)['layout'] | ||
| 22 | else: | ||
| 23 | keymap_layout = parse_keymap_c(keymap_path)['layers'][0]['layout'] | ||
| 24 | return keymap_layout | ||
| 25 | |||
| 26 | |||
| 27 | def _convert_macros(via_macros): | ||
| 28 | via_macros = list(filter(lambda f: bool(f), via_macros)) | ||
| 29 | if len(via_macros) == 0: | ||
| 30 | return list() | ||
| 31 | split_regex = re.compile(r'(}\,)|(\,{)') | ||
| 32 | macros = list() | ||
| 33 | for via_macro in via_macros: | ||
| 34 | # Split VIA macro to its elements | ||
| 35 | macro = split_regex.split(via_macro) | ||
| 36 | # Remove junk elements (None, '},' and ',{') | ||
| 37 | macro = list(filter(lambda f: False if f in (None, '},', ',{') else True, macro)) | ||
| 38 | macro_data = list() | ||
| 39 | for m in macro: | ||
| 40 | if '{' in m or '}' in m: | ||
| 41 | # Found keycode(s) | ||
| 42 | keycodes = m.split(',') | ||
| 43 | # Remove whitespaces and curly braces from around keycodes | ||
| 44 | keycodes = list(map(lambda s: s.strip(' {}'), keycodes)) | ||
| 45 | # Remove the KC prefix | ||
| 46 | keycodes = list(map(lambda s: s.replace('KC_', ''), keycodes)) | ||
| 47 | macro_data.append({"action": "tap", "keycodes": keycodes}) | ||
| 48 | else: | ||
| 49 | # Found text | ||
| 50 | macro_data.append(m) | ||
| 51 | macros.append(macro_data) | ||
| 52 | |||
| 53 | return macros | ||
| 54 | |||
| 55 | |||
| 56 | def _fix_macro_keys(keymap_data): | ||
| 57 | macro_no = re.compile(r'MACRO0?([0-9]{1,2})') | ||
| 58 | for i in range(0, len(keymap_data)): | ||
| 59 | for j in range(0, len(keymap_data[i])): | ||
| 60 | kc = keymap_data[i][j] | ||
| 61 | m = macro_no.match(kc) | ||
| 62 | if m: | ||
| 63 | keymap_data[i][j] = f'MACRO_{m.group(1)}' | ||
| 64 | return keymap_data | ||
| 65 | |||
| 66 | |||
| 67 | def _via_to_keymap(via_backup, keyboard_data, keymap_layout): | ||
| 68 | # Check if passed LAYOUT is correct | ||
| 69 | layout_data = keyboard_data['layouts'].get(keymap_layout) | ||
| 70 | if not layout_data: | ||
| 71 | cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!') | ||
| 72 | exit(1) | ||
| 73 | |||
| 74 | layout_data = layout_data['layout'] | ||
| 75 | sorting_hat = list() | ||
| 76 | for index, data in enumerate(layout_data): | ||
| 77 | sorting_hat.append([index, data['matrix']]) | ||
| 78 | |||
| 79 | sorting_hat.sort(key=lambda k: (k[1][0], k[1][1])) | ||
| 80 | |||
| 81 | pos = 0 | ||
| 82 | for row_num in range(0, keyboard_data['matrix_size']['rows']): | ||
| 83 | for col_num in range(0, keyboard_data['matrix_size']['cols']): | ||
| 84 | if pos >= len(sorting_hat) or sorting_hat[pos][1][0] != row_num or sorting_hat[pos][1][1] != col_num: | ||
| 85 | sorting_hat.insert(pos, [None, [row_num, col_num]]) | ||
| 86 | else: | ||
| 87 | sorting_hat.append([None, [row_num, col_num]]) | ||
| 88 | pos += 1 | ||
| 89 | |||
| 90 | keymap_data = list() | ||
| 91 | for layer in via_backup['layers']: | ||
| 92 | pos = 0 | ||
| 93 | layer_data = list() | ||
| 94 | for key in layer: | ||
| 95 | if sorting_hat[pos][0] is not None: | ||
| 96 | layer_data.append([sorting_hat[pos][0], key]) | ||
| 97 | pos += 1 | ||
| 98 | layer_data.sort() | ||
| 99 | layer_data = [kc[1] for kc in layer_data] | ||
| 100 | keymap_data.append(layer_data) | ||
| 101 | |||
| 102 | return keymap_data | ||
| 103 | |||
| 104 | |||
| 105 | @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') | ||
| 106 | @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") | ||
| 107 | @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='VIA Backup JSON file') | ||
| 108 | @cli.argument('-kb', '--keyboard', type=qmk.keyboard.keyboard_folder, completer=qmk.keyboard.keyboard_completer, arg_only=True, required=True, help='The keyboard\'s name') | ||
| 109 | @cli.argument('-km', '--keymap', arg_only=True, default='via2json', help='The keymap\'s name') | ||
| 110 | @cli.argument('-l', '--layout', arg_only=True, help='The keymap\'s layout') | ||
| 111 | @cli.subcommand('Convert a VIA backup json to keymap.json format.') | ||
| 112 | def via2json(cli): | ||
| 113 | """Convert a VIA backup json to keymap.json format. | ||
| 114 | |||
| 115 | This command uses the `qmk.keymap` module to generate a keymap.json from a VIA backup json. The generated keymap is written to stdout, or to a file if -o is provided. | ||
| 116 | """ | ||
| 117 | # Find appropriate layout macro | ||
| 118 | keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(cli.args.keyboard) | ||
| 119 | if not keymap_layout: | ||
| 120 | cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.") | ||
| 121 | exit(1) | ||
| 122 | |||
| 123 | # Load the VIA backup json | ||
| 124 | with cli.args.filename.open('r') as fd: | ||
| 125 | via_backup = json.load(fd) | ||
| 126 | |||
| 127 | # Generate keyboard metadata | ||
| 128 | keyboard_data = info_json(cli.args.keyboard) | ||
| 129 | |||
| 130 | # Get keycode array | ||
| 131 | keymap_data = _via_to_keymap(via_backup, keyboard_data, keymap_layout) | ||
| 132 | |||
| 133 | # Convert macros | ||
| 134 | macro_data = list() | ||
| 135 | if via_backup.get('macros'): | ||
| 136 | macro_data = _convert_macros(via_backup['macros']) | ||
| 137 | |||
| 138 | # Replace VIA macro keys with JSON keymap ones | ||
| 139 | keymap_data = _fix_macro_keys(keymap_data) | ||
| 140 | |||
| 141 | # Generate the keymap.json | ||
| 142 | keymap_json = generate_json(cli.args.keymap, cli.args.keyboard, keymap_layout, keymap_data, macro_data) | ||
| 143 | |||
| 144 | keymap_lines = [json.dumps(keymap_json, cls=KeymapJSONEncoder)] | ||
| 145 | dump_lines(cli.args.output, keymap_lines, cli.args.quiet) | ||
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 72e91973a3..40a5c1dea8 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py | |||
| @@ -146,7 +146,13 @@ class KeymapJSONEncoder(QMKJSONEncoder): | |||
| 146 | if key == 'JSON_NEWLINE': | 146 | if key == 'JSON_NEWLINE': |
| 147 | layer.append([]) | 147 | layer.append([]) |
| 148 | else: | 148 | else: |
| 149 | layer[-1].append(f'"{key}"') | 149 | if isinstance(key, dict): |
| 150 | # We have a macro | ||
| 151 | |||
| 152 | # TODO: Add proper support for nicely formatting keymap.json macros | ||
| 153 | layer[-1].append(f'{self.encode(key)}') | ||
| 154 | else: | ||
| 155 | layer[-1].append(f'"{key}"') | ||
| 150 | 156 | ||
| 151 | layer = [f"{self.indent_str*indent_level}{', '.join(row)}" for row in layer] | 157 | layer = [f"{self.indent_str*indent_level}{', '.join(row)}" for row in layer] |
| 152 | 158 | ||
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 00b5a78a5a..ca5be0959b 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py | |||
| @@ -158,7 +158,7 @@ def is_keymap_dir(keymap, c=True, json=True, additional_files=None): | |||
| 158 | return True | 158 | return True |
| 159 | 159 | ||
| 160 | 160 | ||
| 161 | def generate_json(keymap, keyboard, layout, layers): | 161 | def generate_json(keymap, keyboard, layout, layers, macros=None): |
| 162 | """Returns a `keymap.json` for the specified keyboard, layout, and layers. | 162 | """Returns a `keymap.json` for the specified keyboard, layout, and layers. |
| 163 | 163 | ||
| 164 | Args: | 164 | Args: |
| @@ -173,11 +173,16 @@ def generate_json(keymap, keyboard, layout, layers): | |||
| 173 | 173 | ||
| 174 | layers | 174 | layers |
| 175 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. | 175 | An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. |
| 176 | |||
| 177 | macros | ||
| 178 | A sequence of strings containing macros to implement for this keyboard. | ||
| 176 | """ | 179 | """ |
| 177 | new_keymap = template_json(keyboard) | 180 | new_keymap = template_json(keyboard) |
| 178 | new_keymap['keymap'] = keymap | 181 | new_keymap['keymap'] = keymap |
| 179 | new_keymap['layout'] = layout | 182 | new_keymap['layout'] = layout |
| 180 | new_keymap['layers'] = layers | 183 | new_keymap['layers'] = layers |
| 184 | if macros: | ||
| 185 | new_keymap['macros'] = macros | ||
| 181 | 186 | ||
| 182 | return new_keymap | 187 | return new_keymap |
| 183 | 188 | ||
