Add cli convert subcommand, from raw KLE to JSON (#6898)

* Add initial pass at KLE convert

* Add cli log on convert

* Move kle2xy, add absolute filepath arg support

* Add overwrite flag, and context sensitive conversion

* Update docs/cli.md

* Fix converter.py typo

* Add convert unit test

* Rename to kle2qmk

* Rename subcommand

* Rename subcommand to kle2json

* Change tests to cover rename

* Rename in __init__.py

* Update CLI docs with new subcommand name

* Fix from suggestions in PR #6898

* Help with cases of case sensitivity

* Update cli.md

* Use angle brackets to indicate required option

* Make the output text more accurate
This commit is contained in:
Cody Bender 2019-11-12 21:55:41 -07:00 committed by skullydazed
parent 00fb1bd1f0
commit 7329c2d02d
8 changed files with 298 additions and 0 deletions

View file

@ -135,6 +135,28 @@ Creates a keymap.c from a QMK Configurator export.
qmk json-keymap [-o OUTPUT] filename
```
## `qmk kle2json`
This command allows you to convert from raw KLE data to QMK Configurator JSON. It accepts either an absolute file path, or a file name in the current directory. By default it will not overwrite `info.json` if it is already present. Use the `-f` or `--force` flag to overwrite.
**Usage**:
```
qmk kle2json [-f] <filename>
```
**Examples**:
```
$ qmk kle2json kle.txt
☒ File info.json already exists, use -f or --force to overwrite.
```
```
$ qmk kle2json -f kle.txt -f
Ψ Wrote out to info.json
```
## `qmk list-keyboards`
This command lists all the keyboards currently defined in `qmk_firmware`

155
lib/python/kle2xy.py Normal file
View file

@ -0,0 +1,155 @@
""" Original code from https://github.com/skullydazed/kle2xy
"""
import hjson
from decimal import Decimal
class KLE2xy(list):
"""Abstract interface for interacting with a KLE layout.
"""
def __init__(self, layout=None, name='', invert_y=True):
super(KLE2xy, self).__init__()
self.name = name
self.invert_y = invert_y
self.key_width = Decimal('19.05')
self.key_skel = {
'decal': False,
'border_color': 'none',
'keycap_profile': '',
'keycap_color': 'grey',
'label_color': 'black',
'label_size': 3,
'label_style': 4,
'width': Decimal('1'), 'height': Decimal('1'),
'x': Decimal('0'), 'y': Decimal('0')
}
self.rows = Decimal(0)
self.columns = Decimal(0)
if layout:
self.parse_layout(layout)
@property
def width(self):
"""Returns the width of the keyboard plate.
"""
return (Decimal(self.columns) * self.key_width) + self.key_width/2
@property
def height(self):
"""Returns the height of the keyboard plate.
"""
return (self.rows * self.key_width) + self.key_width/2
@property
def size(self):
"""Returns the size of the keyboard plate.
"""
return (self.width, self.height)
def attrs(self, properties):
"""Parse the keyboard properties dictionary.
"""
# FIXME: Store more than just the keyboard name.
if 'name' in properties:
self.name = properties['name']
def parse_layout(self, layout):
# Wrap this in a dictionary so hjson will parse KLE raw data
layout = '{"layout": [' + layout + ']}'
layout = hjson.loads(layout)['layout']
# Initialize our state machine
current_key = self.key_skel.copy()
current_row = Decimal(0)
current_col = Decimal(0)
current_x = 0
current_y = self.key_width / 2
if isinstance(layout[0], dict):
self.attrs(layout[0])
layout = layout[1:]
for row_num, row in enumerate(layout):
self.append([])
# Process the current row
for key in row:
if isinstance(key, dict):
if 'w' in key and key['w'] != Decimal(1):
current_key['width'] = Decimal(key['w'])
if 'w2' in key and 'h2' in key and key['w2'] == 1.5 and key['h2'] == 1:
# FIXME: ISO Key uses these params: {x:0.25,w:1.25,h:2,w2:1.5,h2:1,x2:-0.25}
current_key['isoenter'] = True
if 'h' in key and key['h'] != Decimal(1):
current_key['height'] = Decimal(key['h'])
if 'a' in key:
current_key['label_style'] = self.key_skel['label_style'] = int(key['a'])
if current_key['label_style'] < 0:
current_key['label_style'] = 0
elif current_key['label_style'] > 9:
current_key['label_style'] = 9
if 'f' in key:
font_size = int(key['f'])
if font_size > 9:
font_size = 9
elif font_size < 1:
font_size = 1
current_key['label_size'] = self.key_skel['label_size'] = font_size
if 'p' in key:
current_key['keycap_profile'] = self.key_skel['keycap_profile'] = key['p']
if 'c' in key:
current_key['keycap_color'] = self.key_skel['keycap_color'] = key['c']
if 't' in key:
# FIXME: Need to do better validation, plus figure out how to support multiple colors
if '\n' in key['t']:
key['t'] = key['t'].split('\n')[0]
if key['t'] == "0":
key['t'] = "#000000"
current_key['label_color'] = self.key_skel['label_color'] = key['t']
if 'x' in key:
current_col += Decimal(key['x'])
current_x += Decimal(key['x']) * self.key_width
if 'y' in key:
current_row += Decimal(key['y'])
current_y += Decimal(key['y']) * self.key_width
if 'd' in key:
current_key['decal'] = True
else:
current_key['name'] = key
current_key['row'] = current_row
current_key['column'] = current_col
# Determine the X center
x_center = (current_key['width'] * self.key_width) / 2
current_x += x_center
current_key['x'] = current_x
current_x += x_center
# Determine the Y center
y_center = (current_key['height'] * self.key_width) / 2
y_offset = y_center - (self.key_width / 2)
current_key['y'] = (current_y + y_offset)
# Tend to our row/col count
current_col += current_key['width']
if current_col > self.columns:
self.columns = current_col
# Invert the y-axis if neccesary
if self.invert_y:
current_key['y'] = -current_key['y']
# Store this key
self[-1].append(current_key)
current_key = self.key_skel.copy()
# Move to the next row
current_x = 0
current_y += self.key_width
current_col = Decimal(0)
current_row += Decimal(1)
if current_row > self.rows:
self.rows = Decimal(current_row)

View file

@ -10,6 +10,7 @@ from . import doctor
from . import hello
from . import json
from . import list
from . import kle2json
from . import new
from . import pyformat
from . import pytest

79
lib/python/qmk/cli/kle2json.py Executable file
View file

@ -0,0 +1,79 @@
"""Convert raw KLE to JSON
"""
import json
import os
from pathlib import Path
from argparse import FileType
from decimal import Decimal
from collections import OrderedDict
from milc import cli
from kle2xy import KLE2xy
from qmk.converter import kle2qmk
class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj):
try:
if isinstance(obj, Decimal):
if obj % 2 in (Decimal(0), Decimal(1)):
return int(obj)
return float(obj)
except TypeError:
pass
return JSONEncoder.default(self, obj)
@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')
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
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():
return cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', str(file_path))
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.', str(out_path))
return False;
try:
# Convert KLE raw to x/y coordinates (using kle2xy package from skullydazed)
kle = KLE2xy(raw_code)
except Exception as e:
cli.log.error('Could not parse KLE raw data: %s', raw_code)
cli.log.exception(e)
# FIXME: This should be better
return cli.log.error('Could not parse KLE raw data.')
keyboard = OrderedDict(
keyboard_name=kle.name,
url='',
maintainer='qmk',
width=kle.columns,
height=kle.rows,
layouts={'LAYOUT': {
'layout': 'LAYOUT_JSON_HERE'
}},
)
# Initialize keyboard with json encoded from ordered dict
keyboard = json.dumps(keyboard, indent=4, separators=(
', ', ': '), sort_keys=False, cls=CustomJSONEncoder)
# Initialize layout with kle2qmk from converter module
layout = json.dumps(kle2qmk(kle), separators=(
', ', ':'), cls=CustomJSONEncoder)
# Replace layout in keyboard json
keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout)
# Write our info.json
file = open(str(out_path) + "/info.json", "w")
file.write(keyboard)
file.close()
cli.log.info('Wrote out {fg_cyan}%s/info.json', str(out_path))

View file

@ -0,0 +1,33 @@
"""Functions to convert to and from QMK formats
"""
from collections import OrderedDict
def kle2qmk(kle):
"""Convert a KLE layout to QMK's layout format.
"""
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['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)
return layout

View file

@ -0,0 +1,5 @@
["¬\n`","!\n1","\"\n2","£\n3","$\n4","%\n5","^\n6","&\n7","*\n8","(\n9",")\n0","_\n-","+\n=",{w:2},"Backspace"],
[{w:1.5},"Tab","Q","W","E","R","T","Y","U","I","O","P","{\n[","}\n]",{x:0.25,w:1.25,h:2,w2:1.5,h2:1,x2:-0.25},"Enter"],
[{w:1.75},"Caps Lock","A","S","D","F","G","H","J","K","L",":\n;","@\n'","~\n#"],
[{w:1.25},"Shift","|\n\\","Z","X","C","V","B","N","M","<\n,",">\n.","?\n/",{w:2.75},"Shift"],
[{w:1.25},"Ctrl",{w:1.25},"Win",{w:1.25},"Alt",{a:7,w:6.25},"",{a:4,w:1.25},"AltGr",{w:1.25},"Win",{w:1.25},"Menu",{w:1.25},"Ctrl"]

View file

@ -19,6 +19,8 @@ def test_config():
assert result.returncode == 0
assert 'general.color' in result.stdout
def test_kle2json():
assert check_subcommand('kle2json', 'kle.txt', '-f').returncode == 0
def test_doctor():
result = check_subcommand('doctor')

View file

@ -3,3 +3,4 @@
appdirs
argcomplete
colorama
hjson