diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 66d504bfc2..c17c184cc3 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -7,31 +7,78 @@ from pathlib import Path from milc import cli from kle2xy import KLE2xy +import qmk.path from qmk.converter import kle2qmk +from qmk.decorators import automagic_keyboard +from qmk.info import info_json from qmk.info_json_encoder import InfoJSONEncoder -@cli.argument('filename', help='The KLE raw txt to convert') -@cli.argument('-f', '--force', action='store_true', help='Flag to overwrite current info.json') -@cli.subcommand('Convert a KLE layout to a Configurator JSON', hidden=False if cli.config.user.developer else True) +@cli.argument('kle', arg_only=True, help='A file or KLE id to convert') +@cli.argument('--vid', arg_only=True, default='0x03A8', help='USB VID.') +@cli.argument('--pid', arg_only=True, default='0x0000', help='USB PID.') +@cli.argument('-m', '--manufacturer', arg_only=True, default='', help='Manufacturer of the keyboard.') +@cli.argument('-l', '--layout', arg_only=True, default='LAYOUT', help='The LAYOUT name this KLE represents.') +@cli.argument('-kb', '--keyboard', help='The folder for the keyboard.') +@cli.argument('-d', '--diode', arg_only=True, default='COL2ROW', help='The diode direction for the keyboard. (COL2ROW, ROW2COL)') +@cli.subcommand('Use a KLE layout to build info.json and a default keymap', hidden=False if cli.config.user.developer else True) +@automagic_keyboard def kle2json(cli): """Convert a KLE layout to QMK's layout format. - """ # If filename is a path - if cli.args.filename.startswith("/") or cli.args.filename.startswith("./"): - file_path = Path(cli.args.filename) - # Otherwise assume it is a file name + """ + file_path = Path(os.environ['ORIG_CWD'], cli.args.kle) + + # Find our KLE text + if file_path.exists(): + raw_code = file_path.open().read() + else: - file_path = Path(os.environ['ORIG_CWD'], cli.args.filename) - # Check for valid file_path for more graceful failure - if not file_path.exists(): - cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path) - return False - out_path = file_path.parent - raw_code = file_path.open().read() - # Check if info.json exists, allow overwrite with force - if Path(out_path, "info.json").exists() and not cli.args.force: - cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path) + if cli.args.kle.startswith('http') and '#' in cli.args.kle: + kle_path = cli.args.kle.split('#', 1)[1] + if 'gists' not in kle_path: + cli.log.error('Invalid KLE url: {fg_cyan}%s', cli.args.kle) + return False + else: + print('FIXME: fetch gist') + return False + else: + cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path) + return False + + # Make sure the user supplied a keyboard + if not cli.config.kle2json.keyboard: + cli.log.error('You must pass --keyboard or be in a keyboard directory!') + cli.print_usage() return False + + # Check for an existing info.json + if qmk.path.is_keyboard(cli.config.kle2json.keyboard): + kb_info_json = info_json(cli.config.kle2json.keyboard) + else: + kb_info_json = { + "manufacturer": cli.args.manufacturer, + "keyboard_name": cli.config.kle2json.keyboard, + "maintainer": "", + "diode_direction": cli.args.diode, + "features": { + "console": True, + "extrakey": True, + "mousekey": True, + "nkro": True + }, + "matrix_pins": { + "cols": [], + "rows": [], + }, + "usb": { + "device_ver": "0x0001", + "pid": cli.args.pid, + "vid": cli.args.vid + }, + "layouts": {}, + } + + # Build and merge in the new layout try: # Convert KLE raw to x/y coordinates (using kle2xy package from skullydazed) kle = KLE2xy(raw_code) @@ -39,22 +86,18 @@ def kle2json(cli): cli.log.error('Could not parse KLE raw data: %s', raw_code) cli.log.exception(e) return False - keyboard = { - 'keyboard_name': kle.name, - 'url': '', - 'maintainer': 'qmk', - 'width': kle.columns, - 'height': kle.rows, - 'layouts': { - 'LAYOUT': { - 'layout': kle2qmk(kle) - } - }, - } + + if 'layouts' not in kb_info_json: + kb_info_json['layouts'] = {} + + if cli.args.layout not in kb_info_json['layouts']: + kb_info_json['layouts'][cli.args.layout] = {} + + kb_info_json['layouts'][cli.args.layout]['layout'] = kle2qmk(kle) # Write our info.json - keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder) - info_json_file = out_path / 'info.json' - - info_json_file.write_text(keyboard) - cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path) + keyboard_dir = qmk.path.keyboard(cli.config.kle2json.keyboard) + keyboard_dir.mkdir(exist_ok=True, parents=True) + info_json_file = keyboard_dir / 'info.json' + json.dump(kb_info_json, info_json_file.open('w'), indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder) + cli.log.info('Wrote file %s', info_json_file) diff --git a/lib/python/qmk/converter.py b/lib/python/qmk/converter.py index bbd3531317..0e31e17b07 100644 --- a/lib/python/qmk/converter.py +++ b/lib/python/qmk/converter.py @@ -1,33 +1,59 @@ """Functions to convert to and from QMK formats """ -from collections import OrderedDict +from pprint import pprint + +from milc import cli def kle2qmk(kle): """Convert a KLE layout to QMK's layout format. """ layout = [] + layout_alternatives = {} + top_left_corner = None + # Iterate through the KLE classifying keys by layout for row in kle: for key in row: if key['decal']: continue - qmk_key = OrderedDict( - label="", - x=key['column'], - y=key['row'], - ) + if key['label_style'] in [0, 4]: + matrix, _, _, alt_layout, layout_name, _, keycode = key['name'].split('\n') + else: + cli.log.error('Unknown label style: %s', key['label_style']) + continue + matrix = list(map(int, matrix.split(',', 1))) + + qmk_key = { + 'label': keycode, + 'x': key['column'], + 'y': key['row'], + 'matrix': matrix, + } + + if not top_left_corner and (not alt_layout or alt_layout.endswith(',0')): + top_left_corner = key['column'], key['row'] + + # Figure out what layout this key is part of + # FIXME(skullydazed): In the future this will populate `layout_options` in info.json + if alt_layout: + alt_group, layout_index = map(int, alt_layout.split(',', 1)) + if layout_index != 0: + continue + + # Set the key size if key['width'] != 1: qmk_key['w'] = key['width'] if key['height'] != 1: qmk_key['h'] = key['height'] - if 'name' in key and key['name']: - qmk_key['label'] = key['name'].split('\n', 1)[0] - else: - del (qmk_key['label']) layout.append(qmk_key) + # Adjust the keys to account for the top-left corner + for key in layout: + key['x'] -= top_left_corner[0] + key['y'] -= top_left_corner[1] + return layout diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py index 60dae7247f..44d83aa656 100755 --- a/lib/python/qmk/info_json_encoder.py +++ b/lib/python/qmk/info_json_encoder.py @@ -17,6 +17,17 @@ class InfoJSONEncoder(json.JSONEncoder): if not self.indent: self.indent = 4 + def default(self, obj): + """Fix certain objects that don't encode. + """ + if isinstance(obj, Decimal): + if obj == int(obj): + return int(obj) + + return float(obj) + + return json.JSONEncoder.default(self, obj) + def encode(self, obj): """Encode JSON objects for QMK. """