Commit | Line | Data |
---|---|---|
32c78326 MB |
1 | #!/usr/bin/env python |
2 | # -*- coding: utf-8 -*- | |
3 | """Compiler for keymap.c files | |
4 | ||
5 | This scrip will generate a keymap.c file from a simple | |
6 | markdown file with a specific layout. | |
7 | ||
8 | Usage: | |
9 | python compile_keymap.py INPUT_PATH [OUTPUT_PATH] | |
10 | """ | |
20a3229f MB |
11 | from __future__ import division |
12 | from __future__ import print_function | |
13 | from __future__ import absolute_import | |
14 | from __future__ import unicode_literals | |
15 | ||
16 | import os | |
17 | import io | |
18 | import re | |
19 | import sys | |
20 | import json | |
21 | import unicodedata | |
22 | import collections | |
381a9fd5 | 23 | import itertools as it |
20a3229f MB |
24 | |
25 | PY2 = sys.version_info.major == 2 | |
26 | ||
27 | if PY2: | |
32c78326 | 28 | chr = unichr |
20a3229f MB |
29 | |
30 | ||
32c78326 MB |
31 | KEYBOARD_LAYOUTS = { |
32 | # These map positions in the parsed layout to | |
33 | # positions in the KEYMAP MATRIX | |
34 | 'ergodox_ez': [ | |
35 | [ 0, 1, 2, 3, 4, 5, 6], [38, 39, 40, 41, 42, 43, 44], | |
36 | [ 7, 8, 9, 10, 11, 12, 13], [45, 46, 47, 48, 49, 50, 51], | |
37 | [14, 15, 16, 17, 18, 19 ], [ 52, 53, 54, 55, 56, 57], | |
38 | [20, 21, 22, 23, 24, 25, 26], [58, 59, 60, 61, 62, 63, 64], | |
39 | [27, 28, 29, 30, 31 ], [ 65, 66, 67, 68, 69], | |
40 | [ 32, 33], [70, 71 ], | |
41 | [ 34], [72 ], | |
42 | [ 35, 36, 37], [73, 74, 75 ], | |
43 | ] | |
44 | } | |
20a3229f | 45 | |
22691de5 MB |
46 | ROW_INDENTS = { |
47 | 'ergodox_ez': [0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 5, 0, 6, 0, 4, 0] | |
48 | } | |
32c78326 MB |
49 | |
50 | BLANK_LAYOUTS = [ | |
51 | # Compact Layout | |
52 | """ | |
53 | .------------------------------------.------------------------------------. | |
54 | | | | | | | | | | | | | | | | | |
55 | !-----+----+----+----+----+----------!-----+----+----+----+----+----+-----! | |
56 | | | | | | | | | | | | | | | | | |
57 | !-----+----+----+----x----x----! ! !----x----x----+----+----+-----! | |
58 | | | | | | | |-----!-----! | | | | | | | |
59 | !-----+----+----+----x----x----! ! !----x----x----+----+----+-----! | |
60 | | | | | | | | | | | | | | | | | |
61 | '-----+----+----+----+----+----------'----------+----+----+----+----+-----' | |
62 | | | | | | | ! | | | | | | |
63 | '------------------------' '------------------------' | |
64 | .-----------. .-----------. | |
65 | | | | ! | | | |
66 | .-----+-----+-----! !-----+-----+-----. | |
67 | ! ! | | ! | ! ! | |
68 | ! ! !-----! !-----! ! ! | |
69 | | | | | ! | | | | |
70 | '-----------------' '-----------------' | |
71 | """, | |
72 | ||
73 | # Wide Layout | |
74 | """ | |
381a9fd5 MB |
75 | .---------------------------------------------. .---------------------------------------------. |
76 | | | | | | | | | ! | | | | | | | | |
77 | !-------+-----+-----+-----+-----+-------------! !-------+-----+-----+-----+-----+-----+-------! | |
78 | | | | | | | | | ! | | | | | | | | |
79 | !-------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+-------! | |
80 | | | | | | | |-------! !-------! | | | | | | | |
81 | !-------+-----+-----+-----x-----x-----! ! ! !-----x-----x-----+-----+-----+-------! | |
82 | | | | | | | | | ! | | | | | | | | |
83 | '-------+-----+-----+-----+-----+-------------' '-------------+-----+-----+-----+-----+-------' | |
84 | | | | | | | ! | | | | | | |
85 | '------------------------------' '------------------------------' | |
86 | .---------------. .---------------. | |
87 | | | | ! | | | |
88 | .-------+-------+-------! !-------+-------+-------. | |
89 | ! ! | | ! | ! ! | |
90 | ! ! !-------! !-------! ! ! | |
91 | | | | | ! | | | | |
92 | '-----------------------' '-----------------------' | |
32c78326 | 93 | """, |
20a3229f MB |
94 | ] |
95 | ||
32c78326 MB |
96 | |
97 | DEFAULT_CONFIG = { | |
20a3229f MB |
98 | "keymaps_includes": [ |
99 | "keymap_common.h", | |
100 | ], | |
381a9fd5 | 101 | 'filler': "-+.'!:x", |
32c78326 | 102 | 'separator': "|", |
20a3229f | 103 | 'default_key_prefix': ["KC_"], |
20a3229f MB |
104 | } |
105 | ||
20a3229f | 106 | |
32c78326 MB |
107 | SECTIONS = [ |
108 | 'layout_config', | |
109 | 'layers', | |
110 | ] | |
20a3229f | 111 | |
20a3229f | 112 | |
32c78326 | 113 | # Markdown Parsing |
20a3229f | 114 | |
381a9fd5 MB |
115 | ONELINE_COMMENT_RE = re.compile(r""" |
116 | ^ # comment must be at the start of the line | |
117 | \s* # arbitrary whitespace | |
118 | // # start of the comment | |
119 | (.*) # the comment | |
120 | $ # until the end of line | |
121 | """, re.MULTILINE | re.VERBOSE | |
122 | ) | |
123 | ||
124 | INLINE_COMMENT_RE = re.compile(r""" | |
125 | ([\,\"\[\]\{\}\d]) # anythig that might end a expression | |
126 | \s+ # comment must be preceded by whitespace | |
127 | // # start of the comment | |
128 | \s # and succeded by whitespace | |
129 | (?:[^\"\]\}\{\[]*) # the comment (except things which might be json) | |
130 | $ # until the end of line | |
131 | """, re.MULTILINE | re.VERBOSE) | |
132 | ||
133 | TRAILING_COMMA_RE = re.compile(r""" | |
134 | , # the comma | |
135 | (?:\s*) # arbitrary whitespace | |
136 | $ # only works if the trailing comma is followed by newline | |
137 | (\s*) # arbitrary whitespace | |
138 | ([\]\}]) # end of an array or object | |
139 | """, re.MULTILINE | re.VERBOSE) | |
140 | ||
141 | ||
32c78326 | 142 | def loads(raw_data): |
32c78326 MB |
143 | if isinstance(raw_data, bytes): |
144 | raw_data = raw_data.decode('utf-8') | |
20a3229f | 145 | |
32c78326 MB |
146 | raw_data = ONELINE_COMMENT_RE.sub(r"", raw_data) |
147 | raw_data = INLINE_COMMENT_RE.sub(r"\1", raw_data) | |
77fa2b00 | 148 | raw_data = TRAILING_COMMA_RE.sub(r"\1\2", raw_data) |
32c78326 | 149 | return json.loads(raw_data) |
20a3229f | 150 | |
20a3229f | 151 | |
32c78326 MB |
152 | def parse_config(path): |
153 | def reset_section(): | |
154 | section.update({ | |
155 | 'name': section.get('name', ""), | |
156 | 'sub_name': "", | |
157 | 'start_line': -1, | |
158 | 'end_line': -1, | |
159 | 'code_lines': [], | |
160 | }) | |
161 | ||
162 | def start_section(line_index, line): | |
163 | end_section() | |
164 | if line.startswith("# "): | |
165 | name = line[2:] | |
166 | elif line.startswith("## "): | |
167 | name = line[3:] | |
381a9fd5 MB |
168 | else: |
169 | name = "" | |
32c78326 MB |
170 | |
171 | name = name.strip().replace(" ", "_").lower() | |
172 | if name in SECTIONS: | |
173 | section['name'] = name | |
174 | else: | |
175 | section['sub_name'] = name | |
176 | section['start_line'] = line_index | |
177 | ||
178 | def end_section(): | |
179 | if section['start_line'] >= 0: | |
180 | if section['name'] == 'layout_config': | |
181 | config.update(loads("\n".join( | |
182 | section['code_lines'] | |
183 | ))) | |
184 | elif section['sub_name'].startswith('layer'): | |
185 | layer_name = section['sub_name'] | |
186 | config['layer_lines'][layer_name] = section['code_lines'] | |
187 | ||
188 | reset_section() | |
189 | ||
190 | def amend_section(line_index, line): | |
191 | section['end_line'] = line_index | |
192 | section['code_lines'].append(line) | |
193 | ||
194 | config = DEFAULT_CONFIG.copy() | |
195 | config.update({ | |
196 | 'layer_lines': collections.OrderedDict(), | |
197 | 'macro_ids': {'UM'}, | |
198 | 'unicode_macros': {}, | |
199 | }) | |
200 | ||
201 | section = {} | |
202 | reset_section() | |
203 | ||
204 | with io.open(path, encoding="utf-8") as fh: | |
205 | for i, line in enumerate(fh): | |
206 | if line.startswith("#"): | |
207 | start_section(i, line) | |
208 | elif line.startswith(" "): | |
209 | amend_section(i, line[4:]) | |
210 | else: | |
211 | # TODO: maybe parse description | |
212 | pass | |
213 | ||
214 | end_section() | |
381a9fd5 | 215 | assert 'layout' in config |
32c78326 MB |
216 | return config |
217 | ||
218 | # header file parsing | |
219 | ||
220 | IF0_RE = re.compile(r""" | |
221 | ^ | |
222 | #if 0 | |
223 | $.*? | |
224 | #endif | |
381a9fd5 | 225 | """, re.MULTILINE | re.DOTALL | re.VERBOSE) |
20a3229f MB |
226 | |
227 | ||
32c78326 MB |
228 | COMMENT_RE = re.compile(r""" |
229 | /\* | |
230 | .*? | |
231 | \*/" | |
381a9fd5 MB |
232 | """, re.MULTILINE | re.DOTALL | re.VERBOSE) |
233 | ||
20a3229f | 234 | |
32c78326 MB |
235 | def read_header_file(path): |
236 | with io.open(path, encoding="utf-8") as fh: | |
237 | data = fh.read() | |
238 | data, _ = COMMENT_RE.subn("", data) | |
239 | data, _ = IF0_RE.subn("", data) | |
240 | return data | |
241 | ||
242 | ||
381a9fd5 | 243 | def regex_partial(re_str_fmt, flags): |
32c78326 MB |
244 | def partial(*args, **kwargs): |
245 | re_str = re_str_fmt.format(*args, **kwargs) | |
246 | return re.compile(re_str, flags) | |
247 | return partial | |
248 | ||
249 | ||
250 | KEYDEF_REP = regex_partial(r""" | |
251 | #define | |
252 | \s | |
253 | ( | |
254 | (?:{}) # the prefixes | |
255 | (?:\w+) # the key name | |
256 | ) # capture group end | |
381a9fd5 | 257 | """, re.MULTILINE | re.DOTALL | re.VERBOSE) |
32c78326 MB |
258 | |
259 | ||
260 | ENUM_RE = re.compile(r""" | |
261 | ( | |
262 | enum | |
263 | \s\w+\s | |
264 | \{ | |
265 | .*? # the enum content | |
266 | \} | |
267 | ; | |
268 | ) # capture group end | |
381a9fd5 | 269 | """, re.MULTILINE | re.DOTALL | re.VERBOSE) |
32c78326 MB |
270 | |
271 | ||
272 | ENUM_KEY_REP = regex_partial(r""" | |
273 | ( | |
274 | {} # the prefixes | |
275 | \w+ # the key name | |
276 | ) # capture group end | |
381a9fd5 MB |
277 | """, re.MULTILINE | re.DOTALL | re.VERBOSE) |
278 | ||
32c78326 MB |
279 | |
280 | def parse_keydefs(config, data): | |
281 | prefix_options = "|".join(config['key_prefixes']) | |
282 | keydef_re = KEYDEF_REP(prefix_options) | |
283 | enum_key_re = ENUM_KEY_REP(prefix_options) | |
284 | for match in keydef_re.finditer(data): | |
285 | yield match.groups()[0] | |
286 | ||
287 | for enum_match in ENUM_RE.finditer(data): | |
288 | enum = enum_match.groups()[0] | |
289 | for key_match in enum_key_re.finditer(enum): | |
290 | yield key_match.groups()[0] | |
291 | ||
292 | ||
381a9fd5 MB |
293 | def parse_valid_keys(config, out_path): |
294 | basepath = os.path.abspath(os.path.join(os.path.dirname(out_path))) | |
295 | dirpaths = [] | |
296 | subpaths = [] | |
297 | while len(subpaths) < 6: | |
298 | path = os.path.join(basepath, *subpaths) | |
299 | dirpaths.append(path) | |
300 | dirpaths.append(os.path.join(path, "tmk_core", "common")) | |
301 | dirpaths.append(os.path.join(path, "quantum")) | |
302 | subpaths.append('..') | |
303 | ||
304 | includes = set(config['keymaps_includes']) | |
305 | includes.add("keycode.h") | |
32c78326 | 306 | |
381a9fd5 MB |
307 | valid_keycodes = set() |
308 | for dirpath, include in it.product(dirpaths, includes): | |
309 | include_path = os.path.join(dirpath, include) | |
310 | if os.path.exists(include_path): | |
311 | header_data = read_header_file(include_path) | |
32c78326 MB |
312 | valid_keycodes.update( |
313 | parse_keydefs(config, header_data) | |
314 | ) | |
315 | return valid_keycodes | |
316 | ||
381a9fd5 | 317 | |
32c78326 MB |
318 | # Keymap Parsing |
319 | ||
320 | def iter_raw_codes(layer_lines, filler, separator): | |
321 | filler_re = re.compile("[" + filler + " ]") | |
322 | for line in layer_lines: | |
323 | line, _ = filler_re.subn("", line.strip()) | |
324 | if not line: | |
325 | continue | |
326 | codes = line.split(separator) | |
327 | for code in codes[1:-1]: | |
328 | yield code | |
329 | ||
330 | ||
331 | def iter_indexed_codes(raw_codes, key_indexes): | |
332 | key_rows = {} | |
333 | key_indexes_flat = [] | |
381a9fd5 | 334 | |
32c78326 MB |
335 | for row_index, key_indexes in enumerate(key_indexes): |
336 | for key_index in key_indexes: | |
337 | key_rows[key_index] = row_index | |
338 | key_indexes_flat.extend(key_indexes) | |
339 | assert len(raw_codes) == len(key_indexes_flat) | |
340 | for raw_code, key_index in zip(raw_codes, key_indexes_flat): | |
341 | # we keep track of the row mostly for layout purposes | |
342 | yield raw_code, key_index, key_rows[key_index] | |
343 | ||
344 | ||
345 | LAYER_CHANGE_RE = re.compile(r""" | |
346 | (DF|TG|MO)\(\d+\) | |
347 | """, re.VERBOSE) | |
348 | ||
349 | ||
350 | MACRO_RE = re.compile(r""" | |
351 | M\(\w+\) | |
352 | """, re.VERBOSE) | |
353 | ||
354 | ||
355 | UNICODE_RE = re.compile(r""" | |
356 | U[0-9A-F]{4} | |
357 | """, re.VERBOSE) | |
358 | ||
359 | ||
360 | NON_CODE = re.compile(r""" | |
361 | ^[^A-Z0-9_]$ | |
362 | """, re.VERBOSE) | |
363 | ||
364 | ||
365 | def parse_uni_code(raw_code): | |
366 | macro_id = "UC_" + ( | |
367 | unicodedata.name(raw_code) | |
368 | .replace(" ", "_") | |
369 | .replace("-", "_") | |
370 | ) | |
371 | code = "M({})".format(macro_id) | |
372 | uc_hex = "{:04X}".format(ord(raw_code)) | |
373 | return code, macro_id, uc_hex | |
374 | ||
375 | ||
376 | def parse_key_code(raw_code, key_prefixes, valid_keycodes): | |
377 | if raw_code in valid_keycodes: | |
378 | return raw_code | |
379 | ||
380 | for prefix in key_prefixes: | |
381 | code = prefix + raw_code | |
382 | if code in valid_keycodes: | |
383 | return code | |
384 | ||
385 | ||
386 | def parse_code(raw_code, key_prefixes, valid_keycodes): | |
387 | if not raw_code: | |
388 | return 'KC_TRNS', None, None | |
389 | ||
390 | if LAYER_CHANGE_RE.match(raw_code): | |
391 | return raw_code, None, None | |
392 | ||
393 | if MACRO_RE.match(raw_code): | |
381a9fd5 MB |
394 | macro_id = raw_code[2:-1] |
395 | return raw_code, macro_id, None | |
32c78326 MB |
396 | |
397 | if UNICODE_RE.match(raw_code): | |
398 | hex_code = raw_code[1:] | |
399 | return parse_uni_code(chr(int(hex_code, 16))) | |
400 | ||
401 | if NON_CODE.match(raw_code): | |
402 | return parse_uni_code(raw_code) | |
403 | ||
404 | code = parse_key_code(raw_code, key_prefixes, valid_keycodes) | |
405 | return code, None, None | |
406 | ||
407 | ||
408 | def parse_keymap(config, key_indexes, layer_lines, valid_keycodes): | |
409 | keymap = {} | |
410 | raw_codes = list(iter_raw_codes( | |
411 | layer_lines, config['filler'], config['separator'] | |
412 | )) | |
413 | indexed_codes = iter_indexed_codes(raw_codes, key_indexes) | |
381a9fd5 | 414 | key_prefixes = config['key_prefixes'] |
32c78326 MB |
415 | for raw_code, key_index, row_index in indexed_codes: |
416 | code, macro_id, uc_hex = parse_code( | |
381a9fd5 MB |
417 | raw_code, key_prefixes, valid_keycodes |
418 | ) | |
419 | # TODO: line numbers for invalid codes | |
420 | err_msg = "Could not parse key '{}' on row {}".format( | |
421 | raw_code, row_index | |
32c78326 | 422 | ) |
381a9fd5 MB |
423 | assert code is not None, err_msg |
424 | # print(repr(raw_code), repr(code), macro_id, uc_hex) | |
32c78326 MB |
425 | if macro_id: |
426 | config['macro_ids'].add(macro_id) | |
427 | if uc_hex: | |
428 | config['unicode_macros'][macro_id] = uc_hex | |
429 | keymap[key_index] = (code, row_index) | |
430 | return keymap | |
20a3229f | 431 | |
20a3229f | 432 | |
32c78326 MB |
433 | def parse_keymaps(config, valid_keycodes): |
434 | keymaps = collections.OrderedDict() | |
435 | key_indexes = config.get( | |
436 | 'key_indexes', KEYBOARD_LAYOUTS[config['layout']] | |
437 | ) | |
438 | # TODO: maybe validate key_indexes | |
20a3229f | 439 | |
32c78326 MB |
440 | for layer_name, layer_lines, in config['layer_lines'].items(): |
441 | keymaps[layer_name] = parse_keymap( | |
442 | config, key_indexes, layer_lines, valid_keycodes | |
443 | ) | |
444 | return keymaps | |
445 | ||
446 | # keymap.c output | |
20a3229f MB |
447 | |
448 | USERCODE = """ | |
449 | // Runs just one time when the keyboard initializes. | |
32c78326 | 450 | void matrix_init_user(void) { |
20a3229f MB |
451 | |
452 | }; | |
453 | ||
454 | // Runs constantly in the background, in a loop. | |
32c78326 | 455 | void matrix_scan_user(void) { |
20a3229f MB |
456 | uint8_t layer = biton32(layer_state); |
457 | ||
458 | ergodox_board_led_off(); | |
459 | ergodox_right_led_1_off(); | |
460 | ergodox_right_led_2_off(); | |
461 | ergodox_right_led_3_off(); | |
462 | switch (layer) { | |
463 | case L1: | |
464 | ergodox_right_led_1_on(); | |
465 | break; | |
466 | case L2: | |
467 | ergodox_right_led_2_on(); | |
468 | break; | |
469 | case L3: | |
470 | ergodox_right_led_3_on(); | |
471 | break; | |
472 | case L4: | |
473 | ergodox_right_led_1_on(); | |
474 | ergodox_right_led_2_on(); | |
475 | break; | |
476 | case L5: | |
477 | ergodox_right_led_1_on(); | |
478 | ergodox_right_led_3_on(); | |
479 | break; | |
480 | // case L6: | |
481 | // ergodox_right_led_2_on(); | |
482 | // ergodox_right_led_3_on(); | |
483 | // break; | |
484 | // case L7: | |
485 | // ergodox_right_led_1_on(); | |
486 | // ergodox_right_led_2_on(); | |
487 | // ergodox_right_led_3_on(); | |
488 | // break; | |
489 | default: | |
490 | ergodox_board_led_off(); | |
491 | break; | |
492 | } | |
493 | }; | |
494 | """ | |
495 | ||
32c78326 MB |
496 | MACROCODE = """ |
497 | #define UC_MODE_WIN 0 | |
498 | #define UC_MODE_LINUX 1 | |
381a9fd5 | 499 | #define UC_MODE_OSX 2 |
20a3229f | 500 | |
381a9fd5 | 501 | // TODO: allow default mode to be configured |
32c78326 | 502 | static uint16_t unicode_mode = UC_MODE_WIN; |
20a3229f | 503 | |
381a9fd5 MB |
504 | uint16_t hextokeycode(uint8_t hex) {{ |
505 | if (hex == 0x0) {{ | |
506 | return KC_P0; | |
507 | }} | |
508 | if (hex < 0xA) {{ | |
509 | return KC_P1 + (hex - 0x1); | |
510 | }} | |
511 | return KC_A + (hex - 0xA); | |
512 | }} | |
513 | ||
514 | void unicode_action_function(uint16_t hi, uint16_t lo) {{ | |
515 | switch (unicode_mode) {{ | |
516 | case UC_MODE_WIN: | |
517 | register_code(KC_LALT); | |
518 | ||
519 | register_code(KC_PPLS); | |
520 | unregister_code(KC_PPLS); | |
521 | ||
522 | register_code(hextokeycode((hi & 0xF0) >> 4)); | |
523 | unregister_code(hextokeycode((hi & 0xF0) >> 4)); | |
524 | register_code(hextokeycode((hi & 0x0F))); | |
525 | unregister_code(hextokeycode((hi & 0x0F))); | |
526 | register_code(hextokeycode((lo & 0xF0) >> 4)); | |
527 | unregister_code(hextokeycode((lo & 0xF0) >> 4)); | |
528 | register_code(hextokeycode((lo & 0x0F))); | |
529 | unregister_code(hextokeycode((lo & 0x0F))); | |
530 | ||
531 | unregister_code(KC_LALT); | |
532 | break; | |
533 | case UC_MODE_LINUX: | |
534 | register_code(KC_LCTL); | |
535 | register_code(KC_LSFT); | |
536 | ||
537 | register_code(KC_U); | |
538 | unregister_code(KC_U); | |
539 | ||
540 | register_code(hextokeycode((hi & 0xF0) >> 4)); | |
541 | unregister_code(hextokeycode((hi & 0xF0) >> 4)); | |
542 | register_code(hextokeycode((hi & 0x0F))); | |
543 | unregister_code(hextokeycode((hi & 0x0F))); | |
544 | register_code(hextokeycode((lo & 0xF0) >> 4)); | |
545 | unregister_code(hextokeycode((lo & 0xF0) >> 4)); | |
546 | register_code(hextokeycode((lo & 0x0F))); | |
547 | unregister_code(hextokeycode((lo & 0x0F))); | |
548 | ||
549 | unregister_code(KC_LCTL); | |
550 | unregister_code(KC_LSFT); | |
551 | break; | |
552 | case UC_MODE_OSX: | |
553 | break; | |
554 | }} | |
555 | }} | |
556 | ||
32c78326 MB |
557 | const macro_t *action_get_macro(keyrecord_t *record, uint8_t id, uint8_t opt) {{ |
558 | if (!record->event.pressed) {{ | |
559 | return MACRO_NONE; | |
560 | }} | |
561 | // MACRODOWN only works in this function | |
562 | switch(id) {{ | |
563 | case UM: | |
564 | unicode_mode = (unicode_mode + 1) % 2; | |
565 | break; | |
381a9fd5 MB |
566 | {macro_cases} |
567 | {unicode_macro_cases} | |
32c78326 MB |
568 | default: |
569 | break; | |
570 | }} | |
32c78326 MB |
571 | return MACRO_NONE; |
572 | }}; | |
573 | """ | |
20a3229f MB |
574 | |
575 | ||
381a9fd5 MB |
576 | UNICODE_MACRO_TEMPLATE = """ |
577 | case {macro_id}: | |
578 | unicode_action_function(0x{hi:02x}, 0x{lo:02x}); | |
579 | break; | |
580 | """.strip() | |
32c78326 | 581 | |
32c78326 | 582 | |
381a9fd5 | 583 | def unicode_macro_cases(config): |
32c78326 | 584 | for macro_id, uc_hex in config['unicode_macros'].items(): |
381a9fd5 MB |
585 | hi = int(uc_hex, 16) >> 8 |
586 | lo = int(uc_hex, 16) & 0xFF | |
32c78326 MB |
587 | unimacro_keys = ", ".join( |
588 | "T({})".format( | |
589 | "KP_" + digit if digit.isdigit() else digit | |
590 | ) for digit in uc_hex | |
591 | ) | |
381a9fd5 MB |
592 | yield UNICODE_MACRO_TEMPLATE.format( |
593 | macro_id=macro_id, hi=hi, lo=lo | |
594 | ) | |
32c78326 MB |
595 | |
596 | ||
22691de5 MB |
597 | def iter_keymap_lines(keymap, row_indents=None): |
598 | col_widths = {} | |
599 | col = 0 | |
600 | # first pass, figure out the column widths | |
601 | prev_row_index = None | |
602 | for code, row_index in keymap.values(): | |
603 | if row_index != prev_row_index: | |
604 | col = 0 | |
605 | if row_indents: | |
606 | col = row_indents[row_index] | |
607 | col_widths[col] = max(len(code), col_widths.get(col, 0)) | |
608 | prev_row_index = row_index | |
609 | col += 1 | |
610 | ||
611 | # second pass, yield the cell values | |
612 | col = 0 | |
32c78326 MB |
613 | prev_row_index = None |
614 | for key_index in sorted(keymap): | |
615 | code, row_index = keymap[key_index] | |
616 | if row_index != prev_row_index: | |
22691de5 | 617 | col = 0 |
32c78326 | 618 | yield "\n" |
22691de5 MB |
619 | if row_indents: |
620 | for indent_col in range(row_indents[row_index]): | |
621 | pad = " " * (col_widths[indent_col] - 4) | |
622 | yield (" /*-*/" + pad) | |
623 | col = row_indents[row_index] | |
624 | else: | |
625 | yield pad | |
32c78326 MB |
626 | yield " {}".format(code) |
627 | if key_index < len(keymap) - 1: | |
628 | yield "," | |
22691de5 MB |
629 | # This will be yielded on the next iteration when |
630 | # we know that we're not at the end of a line. | |
631 | pad = " " * (col_widths[col] - len(code)) | |
32c78326 | 632 | prev_row_index = row_index |
22691de5 | 633 | col += 1 |
32c78326 MB |
634 | |
635 | ||
636 | def iter_keymap_parts(config, keymaps): | |
637 | # includes | |
638 | for include_path in config['keymaps_includes']: | |
639 | yield '#include "{}"\n'.format(include_path) | |
640 | ||
641 | yield "\n" | |
642 | ||
643 | # definitions | |
644 | for i, macro_id in enumerate(sorted(config['macro_ids'])): | |
645 | yield "#define {} {}\n".format(macro_id, i) | |
646 | ||
647 | yield "\n" | |
648 | ||
649 | for i, layer_name in enumerate(config['layer_lines']): | |
650 | yield '#define L{0:<3} {0:<5} // {1}\n'.format(i, layer_name) | |
651 | ||
652 | yield "\n" | |
653 | ||
654 | # keymaps | |
655 | yield "const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\n" | |
656 | ||
657 | for i, layer_name in enumerate(config['layer_lines']): | |
658 | # comment | |
659 | layer_lines = config['layer_lines'][layer_name] | |
660 | prefixed_lines = " * " + " * ".join(layer_lines) | |
22691de5 | 661 | yield "/*\n{} */\n".format(prefixed_lines) |
32c78326 MB |
662 | |
663 | # keymap codes | |
664 | keymap = keymaps[layer_name] | |
22691de5 MB |
665 | row_indents = ROW_INDENTS.get(config['layout']) |
666 | keymap_lines = "".join(iter_keymap_lines(keymap, row_indents)) | |
32c78326 MB |
667 | yield "[L{0}] = KEYMAP({1}\n),\n".format(i, keymap_lines) |
668 | ||
669 | yield "};\n\n" | |
670 | ||
32c78326 MB |
671 | # macros |
672 | yield MACROCODE.format( | |
673 | macro_cases="", | |
381a9fd5 | 674 | unicode_macro_cases="\n".join(unicode_macro_cases(config)), |
32c78326 MB |
675 | ) |
676 | ||
677 | # TODO: dynamically create blinking lights | |
678 | yield USERCODE | |
679 | ||
680 | ||
681 | def main(argv=sys.argv[1:]): | |
682 | if not argv or '-h' in argv or '--help' in argv: | |
683 | print(__doc__) | |
684 | return 0 | |
685 | ||
686 | in_path = os.path.abspath(argv[0]) | |
687 | if not os.path.exists(in_path): | |
688 | print("No such file '{}'".format(in_path)) | |
689 | return 1 | |
690 | ||
691 | if len(argv) > 1: | |
692 | out_path = os.path.abspath(argv[1]) | |
693 | else: | |
694 | dirname = os.path.dirname(in_path) | |
695 | out_path = os.path.join(dirname, "keymap.c") | |
696 | ||
697 | config = parse_config(in_path) | |
381a9fd5 | 698 | valid_keys = parse_valid_keys(config, out_path) |
32c78326 MB |
699 | keymaps = parse_keymaps(config, valid_keys) |
700 | ||
701 | with io.open(out_path, mode="w", encoding="utf-8") as fh: | |
702 | for part in iter_keymap_parts(config, keymaps): | |
703 | fh.write(part) | |
20a3229f MB |
704 | |
705 | ||
32c78326 MB |
706 | if __name__ == '__main__': |
707 | sys.exit(main()) |