[Keyboard] add keymap beautifier for Ergodox EZ (#4393)
[jackhill/qmk/firmware.git] / keyboards / ergodox_ez / util / keymap_beautifier / KeymapBeautifier.py
1 #!/usr/bin/env python
2
3 import argparse
4 import pycparser
5 import re
6
7 class KeymapBeautifier:
8 justify_toward_center = False
9 filename_in = None
10 filename_out = None
11 output_layout = None
12 output = None
13
14 column_max_widths = {}
15
16 KEY_ALIASES = {
17 "KC_TRANSPARENT": "_______",
18 "KC_TRNS": "_______",
19 "KC_NO": "XXXXXXX",
20 }
21 KEYMAP_START = 'const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\n'
22 KEYMAP_END = '};\n'
23 KEYMAP_START_REPLACEMENT = "const int keymaps[]={\n"
24 KEY_CHART = """
25 /*
26 * ,--------------------------------------------------. ,--------------------------------------------------.
27 * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
28 * |--------+------+------+------+------+------+------| |------+------+------+------+------+------+--------|
29 * | 7 | 8 | 9 | 10 | 11 | 12 | 13 | | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
30 * |--------+------+------+------+------+------| | | |------+------+------+------+------+--------|
31 * | 14 | 15 | 16 | 17 | 18 | 19 |------| |------| 52 | 53 | 54 | 55 | 56 | 57 |
32 * |--------+------+------+------+------+------| 26 | | 58 |------+------+------+------+------+--------|
33 * | 20 | 21 | 22 | 23 | 24 | 25 | | | | 59 | 60 | 61 | 62 | 63 | 64 |
34 * `--------+------+------+------+------+-------------' `-------------+------+------+------+------+--------'
35 * | 27 | 28 | 29 | 30 | 31 | | 65 | 66 | 67 | 68 | 69 |
36 * `----------------------------------' `----------------------------------'
37 * ,-------------. ,-------------.
38 * | 32 | 33 | | 70 | 71 |
39 * ,------+------+------| |------+------+------.
40 * | | | 34 | | 72 | | |
41 * | 35 | 36 |------| |------| 74 | 75 |
42 * | | | 37 | | 73 | | |
43 * `--------------------' `--------------------'
44 */
45 """
46 KEY_COORDINATES = {
47 'LAYOUT_ergodox': [
48 # left hand
49 (0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (0,6),
50 (1,0), (1,1), (1,2), (1,3), (1,4), (1,5), (1,6),
51 (2,0), (2,1), (2,2), (2,3), (2,4), (2,5),
52 (3,0), (3,1), (3,2), (3,3), (3,4), (3,5), (3,6),
53 (4,0), (4,1), (4,2), (4,3), (4,4),
54 # left thumb
55 (5,5), (5,6),
56 (6,6),
57 (7,4), (7,5), (7,6),
58 # right hand
59 (8,0), (8,1), (8,2), (8,3), (8,4), (8,5), (8,6),
60 (9,0), (9,1), (9,2), (9,3), (9,4), (9,5), (9,6),
61 (10,1), (10,2), (10,3), (10,4), (10,5), (10,6),
62 (11,0), (11,1), (11,2), (11,3), (11,4), (11,5), (11,6),
63 (12,2), (12,3), (12,4), (12,5), (12,6),
64 # right thumb
65 (13,0), (13,1),
66 (14,0),
67 (15,0), (15,1), (15,2)
68 ],
69 'LAYOUT_ergodox_pretty': [
70 # left hand and right hand
71 (0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8), (0,9), (0,10), (0,11), (0,12), (0,13),
72 (1,0), (1,1), (1,2), (1,3), (1,4), (1,5), (1,6), (1,7), (1,8), (1,9), (1,10), (1,11), (1,12), (1,13),
73 (2,0), (2,1), (2,2), (2,3), (2,4), (2,5), (2,8), (2,9), (2,10), (2,11), (2,12), (2,13),
74 (3,0), (3,1), (3,2), (3,3), (3,4), (3,5), (3,6), (3,7), (3,8), (3,9), (3,10), (3,11), (3,12), (3,13),
75 (4,0), (4,1), (4,2), (4,3), (4,4), (4,9), (4,10), (4,11), (4,12), (4,13),
76
77 # left thumb and right thumb
78 (5,5), (5,6), (5,7), (5,8),
79 (6,6), (6,7),
80 (7,4), (7,5), (7,6), (7,7), (7,8), (7,9)
81 ],
82 }
83 current_converted_KEY_COORDINATES = []
84
85 # each column is aligned within each group (tuples of row indexes are inclusive)
86 KEY_ROW_GROUPS = {
87 'LAYOUT_ergodox': [(0,4),(5,7),(8,12),(13,15)],
88 'LAYOUT_ergodox_pretty': [(0,7)],
89 #'LAYOUT_ergodox_pretty': [(0,5),(6,7)],
90 #'LAYOUT_ergodox_pretty': [(0,3),(4,4),(5,7)],
91 #'LAYOUT_ergodox_pretty': [(0,4),(5,7)],
92 }
93
94
95 INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox = [
96 0, 1, 2, 3, 4, 5, 6, 38,39,40,41,42,43,44,
97 7, 8, 9,10,11,12,13, 45,46,47,48,49,50,51,
98 14,15,16,17,18,19, 52,53,54,55,56,57,
99 20,21,22,23,24,25,26, 58,59,60,61,62,63,64,
100 27,28,29,30,31, 65,66,67,68,69,
101 32,33, 70,71,
102 34, 72,
103 35,36,37, 73,74,75,
104 ]
105
106
107 def index_conversion_map_reversed(self, conversion_map):
108 return [conversion_map.index(i) for i in range(len(conversion_map))]
109
110
111 def __init__(self, source_code = "", output_layout="LAYOUT_ergodox", justify_toward_center = False):
112 self.output_layout = output_layout
113 self.justify_toward_center = justify_toward_center
114 # determine the conversion map
115 #if input_layout == self.output_layout:
116 # conversion_map = [i for i in range(len(self.INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox))]
117 #conversion_map = self.INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox
118 if self.output_layout == "LAYOUT_ergodox_pretty":
119 index_conversion_map = self.index_conversion_map_reversed(self.INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox)
120 else:
121 index_conversion_map = list(range(len(self.INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox)))
122 self.current_converted_KEY_COORDINATES = [
123 self.KEY_COORDINATES[self.output_layout][index_conversion_map[i]]
124 for i in range(len(self.KEY_COORDINATES[self.output_layout]))
125 ]
126
127 self.output = self.beautify_source_code(source_code)
128
129 def beautify_source_code(self, source_code):
130 # to keep it simple for the parser, we only use the parser to parse the key definition part
131 src = {
132 "before": [],
133 "keys": [],
134 "after": [],
135 }
136
137 current_section = "before"
138 for line in source_code.splitlines(True):
139 if current_section == 'before' and line == self.KEYMAP_START:
140 src[current_section].append("\n")
141 current_section = 'keys'
142 src[current_section].append(self.KEYMAP_START_REPLACEMENT)
143 continue
144 elif current_section == 'keys' and line == self.KEYMAP_END:
145 src[current_section].append(self.KEYMAP_END)
146 current_section = 'after'
147 continue
148 src[current_section].append(line)
149 output_lines = src['before'] + self.beautify_keys_section("".join(src['keys'])) + src['after']
150 return "".join(output_lines)
151
152 def beautify_keys_section(self, src):
153 parsed = self.parser(src)
154 layer_output = []
155
156 keymap = parsed.children()[0]
157 layers = keymap[1]
158 for layer in layers.init.exprs:
159 input_layout = layer.expr.name.name
160
161 key_symbols = self.layer_expr(layer)
162 # re-order keys from input_layout to regular layout
163 if input_layout == "LAYOUT_ergodox_pretty":
164 key_symbols = [key_symbols[i] for i in self.index_conversion_map_reversed(self.INDEX_CONVERSTION_LAYOUT_ergodox_pretty_to_LAYOUT_ergodox)]
165
166 padded_key_symbols = self.pad_key_symbols(key_symbols, input_layout)
167 current_pretty_output_layer = self.pretty_output_layer(layer.name[0].value, padded_key_symbols)
168 # strip trailing spaces from padding
169 layer_output.append(re.sub(r" +\n", "\n", current_pretty_output_layer))
170
171 return [self.KEYMAP_START + "\n",
172 self.KEY_CHART + "\n",
173 ",\n\n".join(layer_output) + "\n",
174 self.KEYMAP_END + "\n"]
175
176 def get_row_group(self, row):
177 for low, high in self.KEY_ROW_GROUPS[self.output_layout]:
178 if low <= row <= high:
179 return (low, high)
180 raise Exception("Cannot find row groups in KEY_ROW_GROUPS")
181
182
183 def calculate_column_max_widths(self, key_symbols):
184 # calculate the max width for each column
185 self.column_max_widths = {}
186 for i in range(len(key_symbols)):
187 row_index, column_index = self.current_converted_KEY_COORDINATES[i]
188 row_group = self.get_row_group(row_index)
189 if (row_group, column_index) in self.column_max_widths:
190 self.column_max_widths[(row_group, column_index)] = max(self.column_max_widths[(row_group, column_index)], len(key_symbols[i]))
191 else:
192 self.column_max_widths[(row_group, column_index)] = len(key_symbols[i])
193
194
195 def pad_key_symbols(self, key_symbols, input_layout, just='left'):
196 self.calculate_column_max_widths(key_symbols)
197
198 padded_key_symbols = []
199 # pad each key symbol
200 for i in range(len(key_symbols)):
201 key = key_symbols[i]
202 # look up column coordinate to determine number of spaces to pad
203 row_index, column_index = self.current_converted_KEY_COORDINATES[i]
204 row_group = self.get_row_group(row_index)
205 if just == 'left':
206 padded_key_symbols.append(key.ljust(self.column_max_widths[(row_group, column_index)]))
207 else:
208 padded_key_symbols.append(key.rjust(self.column_max_widths[(row_group, column_index)]))
209 return padded_key_symbols
210
211
212 layer_keys_pointer = 0
213 layer_keys = None
214 def grab_next_n_columns(self, n_columns, input_layout, layer_keys = None, from_beginning = False):
215 if layer_keys:
216 self.layer_keys = layer_keys
217 if from_beginning:
218 self.layer_keys_pointer = 0
219
220 begin = self.layer_keys_pointer
221 end = begin + n_columns
222 return self.layer_keys[self.layer_keys_pointer-n_keys:self.layer_keys_pointer]
223
224 key_coordinates_counter = 0
225 def get_padded_line(self, source_keys, key_from, key_to, just="left"):
226 if just == "right":
227 keys = [k.strip().rjust(len(k)) for k in source_keys[key_from:key_to]]
228 else:
229 keys = [k for k in source_keys[key_from:key_to]]
230
231 from_row, from_column = self.KEY_COORDINATES[self.output_layout][self.key_coordinates_counter]
232 row_group = self.get_row_group(from_row)
233 self.key_coordinates_counter += key_to - key_from
234 columns_before_key_from = sorted([col for row, col in self.KEY_COORDINATES[self.output_layout] if row == from_row and col < from_column])
235 # figure out which columns in this row needs padding; only pad empty columns to the right of an existing column
236 columns_to_pad = { c: True for c in range(from_column) }
237 if columns_before_key_from:
238 for c in range(max(columns_before_key_from)+1):
239 columns_to_pad[c] = False
240
241 # for rows with fewer columns that don't start with column 0, we need to insert leading spaces
242 spaces = 0
243 for c, v in columns_to_pad.items():
244 if not v:
245 continue
246 if (row_group,c) in self.column_max_widths:
247 spaces += self.column_max_widths[(row_group,c)] + len(", ")
248 else:
249 spaces += 0
250 return " " * spaces + ", ".join(keys) + ","
251
252 def pretty_output_layer(self, layer, keys):
253 self.key_coordinates_counter = 0
254 if self.output_layout == "LAYOUT_ergodox":
255 formatted_key_symbols = """
256 // left hand
257
258 {}
259 {}
260 {}
261 {}
262 {}
263
264 // left thumb
265
266 {}
267 {}
268 {}
269
270 // right hand
271
272 {}
273 {}
274 {}
275 {}
276 {}
277
278 // right thumb
279
280 {}
281 {}
282 {}
283 """.format(
284 # left hand
285 self.get_padded_line(keys, 0, 7, just="left"),
286 self.get_padded_line(keys, 7, 14, just="left"),
287 self.get_padded_line(keys, 14, 20, just="left"),
288 self.get_padded_line(keys, 20, 27, just="left"),
289 self.get_padded_line(keys, 27, 32, just="left"),
290 # left thumb
291 self.get_padded_line(keys, 32, 34, just="left"),
292 self.get_padded_line(keys, 34, 35, just="left"),
293 self.get_padded_line(keys, 35, 38, just="left"),
294 # right hand
295 self.get_padded_line(keys, 38, 45, just="left"),
296 self.get_padded_line(keys, 45, 52, just="left"),
297 self.get_padded_line(keys, 52, 58, just="left"),
298 self.get_padded_line(keys, 58, 65, just="left"),
299 self.get_padded_line(keys, 65, 70, just="left"),
300 # right thumb
301 self.get_padded_line(keys, 70, 72, just="left"),
302 self.get_padded_line(keys, 72, 73, just="left"),
303 self.get_padded_line(keys, 73, 76, just="left"),
304 )
305 elif self.output_layout == "LAYOUT_ergodox_pretty":
306 left_half_justification = "right" if self.justify_toward_center else "left"
307 formatted_key_symbols = """
308 {} {}
309 {} {}
310 {} {}
311 {} {}
312 {} {}
313
314 {} {}
315 {} {}
316 {} {}
317 """.format(
318 self.get_padded_line(keys, 0, 7, just=left_half_justification), self.get_padded_line(keys, 38, 45, just="left"),
319 self.get_padded_line(keys, 7, 14, just=left_half_justification), self.get_padded_line(keys, 45, 52, just="left"),
320 self.get_padded_line(keys, 14, 20, just=left_half_justification), self.get_padded_line(keys, 52, 58, just="left"),
321 self.get_padded_line(keys, 20, 27, just=left_half_justification), self.get_padded_line(keys, 58, 65, just="left"),
322 self.get_padded_line(keys, 27, 32, just=left_half_justification), self.get_padded_line(keys, 65, 70, just="left"),
323
324 self.get_padded_line(keys, 32, 34, just=left_half_justification), self.get_padded_line(keys, 70, 72, just="left"),
325 self.get_padded_line(keys, 34, 35, just=left_half_justification), self.get_padded_line(keys, 72, 73, just="left"),
326 self.get_padded_line(keys, 35, 38, just=left_half_justification), self.get_padded_line(keys, 73, 76, just="left"),
327
328 )
329 else:
330 formatted_key_symbols = ""
331
332 # rid of the trailing comma
333 formatted_key_symbols = formatted_key_symbols[0:len(formatted_key_symbols)-2] + "\n"
334 s = "[{}] = {}({})".format(layer, self.output_layout, formatted_key_symbols)
335 return s
336
337 # helper functions for pycparser
338 def parser(self, src):
339 src = self.comment_remover(src)
340 return pycparser.CParser().parse(src)
341 def comment_remover(self, text):
342 # remove comments since pycparser cannot deal with them
343 # credit: https://stackoverflow.com/a/241506
344 def replacer(match):
345 s = match.group(0)
346 if s.startswith('/'):
347 return " " # note: a space and not an empty string
348 else:
349 return s
350 pattern = re.compile(
351 r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
352 re.DOTALL | re.MULTILINE
353 )
354 return re.sub(pattern, replacer, text)
355
356 def function_expr(self, f):
357 name = f.name.name
358 args = []
359 for arg in f.args.exprs:
360 if type(arg) is pycparser.c_ast.Constant:
361 args.append(arg.value)
362 elif type(arg) is pycparser.c_ast.ID:
363 args.append(arg.name)
364 return "{}({})".format(name, ",".join(args))
365
366 def key_expr(self, raw):
367 if type(raw) is pycparser.c_ast.ID:
368 if raw.name in self.KEY_ALIASES:
369 return self.KEY_ALIASES[raw.name]
370 return raw.name
371 elif type(raw) is pycparser.c_ast.FuncCall:
372 return self.function_expr(raw)
373
374 def layer_expr(self, layer):
375 transformed = [self.key_expr(k) for k in layer.expr.args.exprs]
376 return transformed
377
378
379 if __name__ == "__main__":
380
381 parser = argparse.ArgumentParser(description="Beautify keymap.c downloaded from ErgoDox-Ez Configurator for easier customization.")
382 parser.add_argument("input_filename", help="input file: c source code file that has the layer keymaps")
383 parser.add_argument("-o", "--output-filename", help="output file: beautified c filename. If not given, output to STDOUT.")
384 parser.add_argument("-p", "--pretty-output-layout", action="store_true", help="use LAYOUT_ergodox_pretty for output instead of LAYOUT_ergodox")
385 parser.add_argument("-c", "--justify-toward-center", action="store_true", help="for LAYOUT_ergodox_pretty, align right for the left half, and align left for the right half. Default is align left for both halves.")
386 args = parser.parse_args()
387 if args.pretty_output_layout:
388 output_layout="LAYOUT_ergodox_pretty"
389 else:
390 output_layout="LAYOUT_ergodox"
391 with open(args.input_filename) as f:
392 source_code = f.read()
393 result = KeymapBeautifier(source_code, output_layout=output_layout, justify_toward_center=args.justify_toward_center).output
394 if args.output_filename:
395 with open(args.output_filename, "w") as f:
396 f.write(result)
397 else:
398 print(result)
399