aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
authorCody Bender <50554676+cfbender@users.noreply.github.com>2019-11-12 21:55:41 -0700
committerskullydazed <skullydazed@users.noreply.github.com>2019-11-12 20:55:41 -0800
commit7329c2d02d38f40a23d38f789de34057fd2acd42 (patch)
treebb4e0640164b71d60714b964a72025517c2ade61 /lib/python
parent00fb1bd1f0550645997b61870d7d092494265a60 (diff)
downloadqmk_firmware-7329c2d02d38f40a23d38f789de34057fd2acd42.tar.gz
qmk_firmware-7329c2d02d38f40a23d38f789de34057fd2acd42.zip
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
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/kle2xy.py155
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/kle2json.py79
-rw-r--r--lib/python/qmk/converter.py33
-rw-r--r--lib/python/qmk/tests/kle.txt5
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py2
6 files changed, 275 insertions, 0 deletions
diff --git a/lib/python/kle2xy.py b/lib/python/kle2xy.py
new file mode 100644
index 000000000..ea16a4b5e
--- /dev/null
+++ b/lib/python/kle2xy.py
@@ -0,0 +1,155 @@
1""" Original code from https://github.com/skullydazed/kle2xy
2"""
3
4import hjson
5from decimal import Decimal
6
7class KLE2xy(list):
8 """Abstract interface for interacting with a KLE layout.
9 """
10 def __init__(self, layout=None, name='', invert_y=True):
11 super(KLE2xy, self).__init__()
12
13 self.name = name
14 self.invert_y = invert_y
15 self.key_width = Decimal('19.05')
16 self.key_skel = {
17 'decal': False,
18 'border_color': 'none',
19 'keycap_profile': '',
20 'keycap_color': 'grey',
21 'label_color': 'black',
22 'label_size': 3,
23 'label_style': 4,
24 'width': Decimal('1'), 'height': Decimal('1'),
25 'x': Decimal('0'), 'y': Decimal('0')
26 }
27 self.rows = Decimal(0)
28 self.columns = Decimal(0)
29
30 if layout:
31 self.parse_layout(layout)
32
33 @property
34 def width(self):
35 """Returns the width of the keyboard plate.
36 """
37 return (Decimal(self.columns) * self.key_width) + self.key_width/2
38
39 @property
40 def height(self):
41 """Returns the height of the keyboard plate.
42 """
43 return (self.rows * self.key_width) + self.key_width/2
44
45 @property
46 def size(self):
47 """Returns the size of the keyboard plate.
48 """
49 return (self.width, self.height)
50
51 def attrs(self, properties):
52 """Parse the keyboard properties dictionary.
53 """
54 # FIXME: Store more than just the keyboard name.
55 if 'name' in properties:
56 self.name = properties['name']
57
58 def parse_layout(self, layout):
59 # Wrap this in a dictionary so hjson will parse KLE raw data
60 layout = '{"layout": [' + layout + ']}'
61 layout = hjson.loads(layout)['layout']
62
63 # Initialize our state machine
64 current_key = self.key_skel.copy()
65 current_row = Decimal(0)
66 current_col = Decimal(0)
67 current_x = 0
68 current_y = self.key_width / 2
69
70 if isinstance(layout[0], dict):
71 self.attrs(layout[0])
72 layout = layout[1:]
73
74 for row_num, row in enumerate(layout):
75 self.append([])
76
77 # Process the current row
78 for key in row:
79 if isinstance(key, dict):
80 if 'w' in key and key['w'] != Decimal(1):
81 current_key['width'] = Decimal(key['w'])
82 if 'w2' in key and 'h2' in key and key['w2'] == 1.5 and key['h2'] == 1:
83 # FIXME: ISO Key uses these params: {x:0.25,w:1.25,h:2,w2:1.5,h2:1,x2:-0.25}
84 current_key['isoenter'] = True
85 if 'h' in key and key['h'] != Decimal(1):
86 current_key['height'] = Decimal(key['h'])
87 if 'a' in key:
88 current_key['label_style'] = self.key_skel['label_style'] = int(key['a'])
89 if current_key['label_style'] < 0:
90 current_key['label_style'] = 0
91 elif current_key['label_style'] > 9:
92 current_key['label_style'] = 9
93 if 'f' in key:
94 font_size = int(key['f'])
95 if font_size > 9:
96 font_size = 9
97 elif font_size < 1:
98 font_size = 1
99 current_key['label_size'] = self.key_skel['label_size'] = font_size
100 if 'p' in key:
101 current_key['keycap_profile'] = self.key_skel['keycap_profile'] = key['p']
102 if 'c' in key:
103 current_key['keycap_color'] = self.key_skel['keycap_color'] = key['c']
104 if 't' in key:
105 # FIXME: Need to do better validation, plus figure out how to support multiple colors
106 if '\n' in key['t']:
107 key['t'] = key['t'].split('\n')[0]
108 if key['t'] == "0":
109 key['t'] = "#000000"
110 current_key['label_color'] = self.key_skel['label_color'] = key['t']
111 if 'x' in key:
112 current_col += Decimal(key['x'])
113 current_x += Decimal(key['x']) * self.key_width
114 if 'y' in key:
115 current_row += Decimal(key['y'])
116 current_y += Decimal(key['y']) * self.key_width
117 if 'd' in key:
118 current_key['decal'] = True
119
120 else:
121 current_key['name'] = key
122 current_key['row'] = current_row
123 current_key['column'] = current_col
124
125 # Determine the X center
126 x_center = (current_key['width'] * self.key_width) / 2
127 current_x += x_center
128 current_key['x'] = current_x
129 current_x += x_center
130
131 # Determine the Y center
132 y_center = (current_key['height'] * self.key_width) / 2
133 y_offset = y_center - (self.key_width / 2)
134 current_key['y'] = (current_y + y_offset)
135
136 # Tend to our row/col count
137 current_col += current_key['width']
138 if current_col > self.columns:
139 self.columns = current_col
140
141 # Invert the y-axis if neccesary
142 if self.invert_y:
143 current_key['y'] = -current_key['y']
144
145 # Store this key
146 self[-1].append(current_key)
147 current_key = self.key_skel.copy()
148
149 # Move to the next row
150 current_x = 0
151 current_y += self.key_width
152 current_col = Decimal(0)
153 current_row += Decimal(1)
154 if current_row > self.rows:
155 self.rows = Decimal(current_row)
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index e41cc3dcb..1b83e78c7 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -10,6 +10,7 @@ from . import doctor
10from . import hello 10from . import hello
11from . import json 11from . import json
12from . import list 12from . import list
13from . import kle2json
13from . import new 14from . import new
14from . import pyformat 15from . import pyformat
15from . import pytest 16from . import pytest
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
new file mode 100755
index 000000000..22eb515df
--- /dev/null
+++ b/lib/python/qmk/cli/kle2json.py
@@ -0,0 +1,79 @@
1"""Convert raw KLE to JSON
2
3"""
4import json
5import os
6from pathlib import Path
7from argparse import FileType
8from decimal import Decimal
9from collections import OrderedDict
10
11from milc import cli
12from kle2xy import KLE2xy
13
14from qmk.converter import kle2qmk
15
16
17class CustomJSONEncoder(json.JSONEncoder):
18 def default(self, obj):
19 try:
20 if isinstance(obj, Decimal):
21 if obj % 2 in (Decimal(0), Decimal(1)):
22 return int(obj)
23 return float(obj)
24 except TypeError:
25 pass
26 return JSONEncoder.default(self, obj)
27
28
29@cli.argument('filename', help='The KLE raw txt to convert')
30@cli.argument('-f', '--force', action='store_true', help='Flag to overwrite current info.json')
31@cli.subcommand('Convert a KLE layout to a Configurator JSON')
32def kle2json(cli):
33 """Convert a KLE layout to QMK's layout format.
34 """ # If filename is a path
35 if cli.args.filename.startswith("/") or cli.args.filename.startswith("./"):
36 file_path = Path(cli.args.filename)
37 # Otherwise assume it is a file name
38 else:
39 file_path = Path(os.environ['ORIG_CWD'], cli.args.filename)
40 # Check for valid file_path for more graceful failure
41 if not file_path.exists():
42 return cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', str(file_path))
43 out_path = file_path.parent
44 raw_code = file_path.open().read()
45 # Check if info.json exists, allow overwrite with force
46 if Path(out_path, "info.json").exists() and not cli.args.force:
47 cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', str(out_path))
48 return False;
49 try:
50 # Convert KLE raw to x/y coordinates (using kle2xy package from skullydazed)
51 kle = KLE2xy(raw_code)
52 except Exception as e:
53 cli.log.error('Could not parse KLE raw data: %s', raw_code)
54 cli.log.exception(e)
55 # FIXME: This should be better
56 return cli.log.error('Could not parse KLE raw data.')
57 keyboard = OrderedDict(
58 keyboard_name=kle.name,
59 url='',
60 maintainer='qmk',
61 width=kle.columns,
62 height=kle.rows,
63 layouts={'LAYOUT': {
64 'layout': 'LAYOUT_JSON_HERE'
65 }},
66 )
67 # Initialize keyboard with json encoded from ordered dict
68 keyboard = json.dumps(keyboard, indent=4, separators=(
69 ', ', ': '), sort_keys=False, cls=CustomJSONEncoder)
70 # Initialize layout with kle2qmk from converter module
71 layout = json.dumps(kle2qmk(kle), separators=(
72 ', ', ':'), cls=CustomJSONEncoder)
73 # Replace layout in keyboard json
74 keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout)
75 # Write our info.json
76 file = open(str(out_path) + "/info.json", "w")
77 file.write(keyboard)
78 file.close()
79 cli.log.info('Wrote out {fg_cyan}%s/info.json', str(out_path))
diff --git a/lib/python/qmk/converter.py b/lib/python/qmk/converter.py
new file mode 100644
index 000000000..bbd353131
--- /dev/null
+++ b/lib/python/qmk/converter.py
@@ -0,0 +1,33 @@
1"""Functions to convert to and from QMK formats
2"""
3from collections import OrderedDict
4
5
6def kle2qmk(kle):
7 """Convert a KLE layout to QMK's layout format.
8 """
9 layout = []
10
11 for row in kle:
12 for key in row:
13 if key['decal']:
14 continue
15
16 qmk_key = OrderedDict(
17 label="",
18 x=key['column'],
19 y=key['row'],
20 )
21
22 if key['width'] != 1:
23 qmk_key['w'] = key['width']
24 if key['height'] != 1:
25 qmk_key['h'] = key['height']
26 if 'name' in key and key['name']:
27 qmk_key['label'] = key['name'].split('\n', 1)[0]
28 else:
29 del (qmk_key['label'])
30
31 layout.append(qmk_key)
32
33 return layout
diff --git a/lib/python/qmk/tests/kle.txt b/lib/python/qmk/tests/kle.txt
new file mode 100644
index 000000000..862a899ab
--- /dev/null
+++ b/lib/python/qmk/tests/kle.txt
@@ -0,0 +1,5 @@
1["¬\n`","!\n1","\"\n2","£\n3","$\n4","%\n5","^\n6","&\n7","*\n8","(\n9",")\n0","_\n-","+\n=",{w:2},"Backspace"],
2[{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"],
3[{w:1.75},"Caps Lock","A","S","D","F","G","H","J","K","L",":\n;","@\n'","~\n#"],
4[{w:1.25},"Shift","|\n\\","Z","X","C","V","B","N","M","<\n,",">\n.","?\n/",{w:2.75},"Shift"],
5[{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"]
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index 55b8d253f..d91af992a 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -19,6 +19,8 @@ def test_config():
19 assert result.returncode == 0 19 assert result.returncode == 0
20 assert 'general.color' in result.stdout 20 assert 'general.color' in result.stdout
21 21
22def test_kle2json():
23 assert check_subcommand('kle2json', 'kle.txt', '-f').returncode == 0
22 24
23def test_doctor(): 25def test_doctor():
24 result = check_subcommand('doctor') 26 result = check_subcommand('doctor')