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