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