]> git.donarmstrong.com Git - qmk_firmware.git/blob - keyboards/ergodox/keymaps/german-manuneo/compile_keymap.py
Initial structure for Ergodox as subprojects
[qmk_firmware.git] / keyboards / ergodox / keymaps / german-manuneo / compile_keymap.py
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 """
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
23 import itertools as it
24
25 PY2 = sys.version_info.major == 2
26
27 if PY2:
28     chr = unichr
29
30
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 }
45
46 ROW_INDENTS = {
47     'ergodox_ez': [0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 5, 0, 6, 0, 4, 0]
48 }
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 """
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                       '-----------------------' '-----------------------'
93 """,
94 ]
95
96
97 DEFAULT_CONFIG = {
98     "keymaps_includes": [
99         "keymap_common.h",
100     ],
101     'filler': "-+.'!:x",
102     'separator': "|",
103     'default_key_prefix': ["KC_"],
104 }
105
106
107 SECTIONS = [
108     'layout_config',
109     'layers',
110 ]
111
112
113 #       Markdown Parsing
114
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
142 def loads(raw_data):
143     if isinstance(raw_data, bytes):
144         raw_data = raw_data.decode('utf-8')
145
146     raw_data = ONELINE_COMMENT_RE.sub(r"", raw_data)
147     raw_data = INLINE_COMMENT_RE.sub(r"\1", raw_data)
148     raw_data = TRAILING_COMMA_RE.sub(r"\1\2", raw_data)
149     return json.loads(raw_data)
150
151
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:]
168         else:
169             name = ""
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()
215     assert 'layout' in config
216     return config
217
218 #       header file parsing
219
220 IF0_RE = re.compile(r"""
221     ^
222     #if 0
223     $.*?
224     #endif
225 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
226
227
228 COMMENT_RE = re.compile(r"""
229     /\*
230     .*?
231     \*/"
232 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
233
234
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
243 def regex_partial(re_str_fmt, flags):
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
257 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
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
269 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
270
271
272 ENUM_KEY_REP = regex_partial(r"""
273     (
274         {}              # the prefixes
275         \w+             # the key name
276     )                   # capture group end
277 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
278
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
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")
306
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)
312             valid_keycodes.update(
313                 parse_keydefs(config, header_data)
314             )
315     return valid_keycodes
316
317
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 = []
334
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):
394         macro_id = raw_code[2:-1]
395         return raw_code, macro_id, None
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)
414     key_prefixes = config['key_prefixes']
415     for raw_code, key_index, row_index in indexed_codes:
416         code, macro_id, uc_hex = parse_code(
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
422         )
423         assert code is not None, err_msg
424         # print(repr(raw_code), repr(code), macro_id, uc_hex)
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
431
432
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
439
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
447
448 USERCODE = """
449 // Runs just one time when the keyboard initializes.
450 void matrix_init_user(void) {
451
452 };
453
454 // Runs constantly in the background, in a loop.
455 void matrix_scan_user(void) {
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
496 MACROCODE = """
497 #define UC_MODE_WIN 0
498 #define UC_MODE_LINUX 1
499 #define UC_MODE_OSX 2
500
501 // TODO: allow default mode to be configured
502 static uint16_t unicode_mode = UC_MODE_WIN;
503
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
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;
566 {macro_cases}
567 {unicode_macro_cases}
568         default:
569             break;
570     }}
571     return MACRO_NONE;
572 }};
573 """
574
575
576 UNICODE_MACRO_TEMPLATE = """
577 case {macro_id}:
578     unicode_action_function(0x{hi:02x}, 0x{lo:02x});
579     break;
580 """.strip()
581
582
583 def unicode_macro_cases(config):
584     for macro_id, uc_hex in config['unicode_macros'].items():
585         hi = int(uc_hex, 16) >> 8
586         lo = int(uc_hex, 16) & 0xFF
587         unimacro_keys = ", ".join(
588             "T({})".format(
589                 "KP_" + digit if digit.isdigit() else digit
590             ) for digit in uc_hex
591         )
592         yield UNICODE_MACRO_TEMPLATE.format(
593             macro_id=macro_id, hi=hi, lo=lo
594         )
595
596
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
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:
617             col = 0
618             yield "\n"
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
626         yield " {}".format(code)
627         if key_index < len(keymap) - 1:
628             yield ","
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))
632         prev_row_index = row_index
633         col += 1
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)
661         yield "/*\n{} */\n".format(prefixed_lines)
662
663         # keymap codes
664         keymap = keymaps[layer_name]
665         row_indents = ROW_INDENTS.get(config['layout'])
666         keymap_lines = "".join(iter_keymap_lines(keymap, row_indents))
667         yield "[L{0}] = KEYMAP({1}\n),\n".format(i, keymap_lines)
668
669     yield "};\n\n"
670
671     # no idea what this is for
672     yield "const uint16_t PROGMEM fn_actions[] = {};\n"
673
674     # macros
675     yield MACROCODE.format(
676         macro_cases="",
677         unicode_macro_cases="\n".join(unicode_macro_cases(config)),
678     )
679
680     # TODO: dynamically create blinking lights
681     yield USERCODE
682
683
684 def main(argv=sys.argv[1:]):
685     if not argv or '-h' in argv or '--help' in argv:
686         print(__doc__)
687         return 0
688
689     in_path = os.path.abspath(argv[0])
690     if not os.path.exists(in_path):
691         print("No such file '{}'".format(in_path))
692         return 1
693
694     if len(argv) > 1:
695         out_path = os.path.abspath(argv[1])
696     else:
697         dirname = os.path.dirname(in_path)
698         out_path = os.path.join(dirname, "keymap.c")
699
700     config = parse_config(in_path)
701     valid_keys = parse_valid_keys(config, out_path)
702     keymaps = parse_keymaps(config, valid_keys)
703
704     with io.open(out_path, mode="w", encoding="utf-8") as fh:
705         for part in iter_keymap_parts(config, keymaps):
706             fh.write(part)
707
708
709 if __name__ == '__main__':
710     sys.exit(main())