summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/cli_commands.md17
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/via2json.py145
-rwxr-xr-xlib/python/qmk/json_encoders.py8
-rw-r--r--lib/python/qmk/keymap.py7
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
qmk clean [-a]
```
+## `qmk via2json`
+
+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.
+
+**Usage**:
+
+```
+qmk via2json -kb KEYBOARD [-l LAYOUT] [-km KEYMAP] [-o OUTPUT] filename
+```
+
+**Example:**
+
+```
+$ qmk via2json -kb ai03/polaris -o polaris_keymap.json polaris_via_backup.json
+Ψ Wrote keymap to /home/you/qmk_firmware/polaris_keymap.json
+```
+
---
# 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 = [
'qmk.cli.new.keymap',
'qmk.cli.pyformat',
'qmk.cli.pytest',
+ 'qmk.cli.via2json',
]
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 @@
+"""Generate a keymap.c from a configurator export.
+"""
+import json
+import re
+
+from milc import cli
+
+import qmk.keyboard
+import qmk.path
+from qmk.info import info_json
+from qmk.json_encoders import KeymapJSONEncoder
+from qmk.commands import parse_configurator_json, dump_lines
+from qmk.keymap import generate_json, list_keymaps, locate_keymap, parse_keymap_c
+
+
+def _find_via_layout_macro(keyboard):
+ keymap_layout = None
+ if 'via' in list_keymaps(keyboard):
+ keymap_path = locate_keymap(keyboard, 'via')
+ if keymap_path.suffix == '.json':
+ keymap_layout = parse_configurator_json(keymap_path)['layout']
+ else:
+ keymap_layout = parse_keymap_c(keymap_path)['layers'][0]['layout']
+ return keymap_layout
+
+
+def _convert_macros(via_macros):
+ via_macros = list(filter(lambda f: bool(f), via_macros))
+ if len(via_macros) == 0:
+ return list()
+ split_regex = re.compile(r'(}\,)|(\,{)')
+ macros = list()
+ for via_macro in via_macros:
+ # Split VIA macro to its elements
+ macro = split_regex.split(via_macro)
+ # Remove junk elements (None, '},' and ',{')
+ macro = list(filter(lambda f: False if f in (None, '},', ',{') else True, macro))
+ macro_data = list()
+ for m in macro:
+ if '{' in m or '}' in m:
+ # Found keycode(s)
+ keycodes = m.split(',')
+ # Remove whitespaces and curly braces from around keycodes
+ keycodes = list(map(lambda s: s.strip(' {}'), keycodes))
+ # Remove the KC prefix
+ keycodes = list(map(lambda s: s.replace('KC_', ''), keycodes))
+ macro_data.append({"action": "tap", "keycodes": keycodes})
+ else:
+ # Found text
+ macro_data.append(m)
+ macros.append(macro_data)
+
+ return macros
+
+
+def _fix_macro_keys(keymap_data):
+ macro_no = re.compile(r'MACRO0?([0-9]{1,2})')
+ for i in range(0, len(keymap_data)):
+ for j in range(0, len(keymap_data[i])):
+ kc = keymap_data[i][j]
+ m = macro_no.match(kc)
+ if m:
+ keymap_data[i][j] = f'MACRO_{m.group(1)}'
+ return keymap_data
+
+
+def _via_to_keymap(via_backup, keyboard_data, keymap_layout):
+ # Check if passed LAYOUT is correct
+ layout_data = keyboard_data['layouts'].get(keymap_layout)
+ if not layout_data:
+ cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!')
+ exit(1)
+
+ layout_data = layout_data['layout']
+ sorting_hat = list()
+ for index, data in enumerate(layout_data):
+ sorting_hat.append([index, data['matrix']])
+
+ sorting_hat.sort(key=lambda k: (k[1][0], k[1][1]))
+
+ pos = 0
+ for row_num in range(0, keyboard_data['matrix_size']['rows']):
+ for col_num in range(0, keyboard_data['matrix_size']['cols']):
+ if pos >= len(sorting_hat) or sorting_hat[pos][1][0] != row_num or sorting_hat[pos][1][1] != col_num:
+ sorting_hat.insert(pos, [None, [row_num, col_num]])
+ else:
+ sorting_hat.append([None, [row_num, col_num]])
+ pos += 1
+
+ keymap_data = list()
+ for layer in via_backup['layers']:
+ pos = 0
+ layer_data = list()
+ for key in layer:
+ if sorting_hat[pos][0] is not None:
+ layer_data.append([sorting_hat[pos][0], key])
+ pos += 1
+ layer_data.sort()
+ layer_data = [kc[1] for kc in layer_data]
+ keymap_data.append(layer_data)
+
+ return keymap_data
+
+
+@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
+@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
+@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='VIA Backup JSON file')
+@cli.argument('-kb', '--keyboard', type=qmk.keyboard.keyboard_folder, completer=qmk.keyboard.keyboard_completer, arg_only=True, required=True, help='The keyboard\'s name')
+@cli.argument('-km', '--keymap', arg_only=True, default='via2json', help='The keymap\'s name')
+@cli.argument('-l', '--layout', arg_only=True, help='The keymap\'s layout')
+@cli.subcommand('Convert a VIA backup json to keymap.json format.')
+def via2json(cli):
+ """Convert a VIA backup json to keymap.json format.
+
+ 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.
+ """
+ # Find appropriate layout macro
+ keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(cli.args.keyboard)
+ if not keymap_layout:
+ cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.")
+ exit(1)
+
+ # Load the VIA backup json
+ with cli.args.filename.open('r') as fd:
+ via_backup = json.load(fd)
+
+ # Generate keyboard metadata
+ keyboard_data = info_json(cli.args.keyboard)
+
+ # Get keycode array
+ keymap_data = _via_to_keymap(via_backup, keyboard_data, keymap_layout)
+
+ # Convert macros
+ macro_data = list()
+ if via_backup.get('macros'):
+ macro_data = _convert_macros(via_backup['macros'])
+
+ # Replace VIA macro keys with JSON keymap ones
+ keymap_data = _fix_macro_keys(keymap_data)
+
+ # Generate the keymap.json
+ keymap_json = generate_json(cli.args.keymap, cli.args.keyboard, keymap_layout, keymap_data, macro_data)
+
+ keymap_lines = [json.dumps(keymap_json, cls=KeymapJSONEncoder)]
+ 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):
if key == 'JSON_NEWLINE':
layer.append([])
else:
- layer[-1].append(f'"{key}"')
+ if isinstance(key, dict):
+ # We have a macro
+
+ # TODO: Add proper support for nicely formatting keymap.json macros
+ layer[-1].append(f'{self.encode(key)}')
+ else:
+ layer[-1].append(f'"{key}"')
layer = [f"{self.indent_str*indent_level}{', '.join(row)}" for row in layer]
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):
return True
-def generate_json(keymap, keyboard, layout, layers):
+def generate_json(keymap, keyboard, layout, layers, macros=None):
"""Returns a `keymap.json` for the specified keyboard, layout, and layers.
Args:
@@ -173,11 +173,16 @@ def generate_json(keymap, 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_json(keyboard)
new_keymap['keymap'] = keymap
new_keymap['layout'] = layout
new_keymap['layers'] = layers
+ if macros:
+ new_keymap['macros'] = macros
return new_keymap