forked from qmk/qmk_firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add cli convert subcommand, from raw KLE to JSON (qmk#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 qmk#6898 * Help with cases of case sensitivity * Update cli.md * Use angle brackets to indicate required option * Make the output text more accurate
- Loading branch information
1 parent
00fb1bd
commit 7329c2d
Showing
8 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ | |
appdirs | ||
argcomplete | ||
colorama | ||
hjson |