keyboard stuff
1"""Generate a keymap.c from a configurator export.
2"""
3import json
4import re
5
6from milc import cli
7
8import qmk.keyboard
9import qmk.path
10from qmk.info import info_json
11from qmk.json_encoders import KeymapJSONEncoder
12from qmk.commands import dump_lines
13from qmk.keymap import generate_json
14
15
16def _find_via_layout_macro(keyboard_data):
17 """Assume layout macro when only 1 is available
18 """
19 layouts = list(keyboard_data['layouts'].keys())
20 return layouts[0] if len(layouts) == 1 else None
21
22
23def _convert_macros(via_macros):
24 via_macros = list(filter(lambda f: bool(f), via_macros))
25 if len(via_macros) == 0:
26 return list()
27 split_regex = re.compile(r'(}\,)|(\,{)')
28 macro_group_regex = re.compile(r'({.+?})')
29 macros = list()
30 for via_macro in via_macros:
31 # Split VIA macro to its elements
32 macro = split_regex.split(via_macro)
33 # Remove junk elements (None, '},' and ',{')
34 macro = list(filter(lambda f: False if f in (None, '},', ',{') else True, macro))
35 macro_data = list()
36 for m in macro:
37 if '{' in m or '}' in m:
38 # Split macro groups
39 macro_groups = macro_group_regex.findall(m)
40 for macro_group in macro_groups:
41 # Remove whitespaces and curly braces from around group
42 macro_group = macro_group.strip(' {}')
43
44 macro_action = 'tap'
45 macro_keycodes = []
46
47 if macro_group[0] == '+':
48 macro_action = 'down'
49 macro_keycodes.append(macro_group[1:])
50 elif macro_group[0] == '-':
51 macro_action = 'up'
52 macro_keycodes.append(macro_group[1:])
53 else:
54 macro_keycodes.extend(macro_group.split(',') if ',' in macro_group else [macro_group])
55
56 # Remove the KC prefixes
57 macro_keycodes = list(map(lambda s: s.replace('KC_', ''), macro_keycodes))
58
59 macro_data.append({"action": macro_action, "keycodes": macro_keycodes})
60 else:
61 # Found text
62 macro_data.append(m)
63 macros.append(macro_data)
64
65 return macros
66
67
68def _fix_macro_keys(keymap_data):
69 macro_no = re.compile(r'MACRO0?\(([0-9]{1,2})\)')
70 for i in range(0, len(keymap_data)):
71 for j in range(0, len(keymap_data[i])):
72 kc = keymap_data[i][j]
73 m = macro_no.match(kc)
74 if m:
75 keymap_data[i][j] = f'MC_{m.group(1)}'
76 return keymap_data
77
78
79def _via_to_keymap(via_backup, keyboard_data, keymap_layout):
80 # Check if passed LAYOUT is correct
81 layout_data = keyboard_data['layouts'].get(keymap_layout)
82 if not layout_data:
83 cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!')
84 return None
85
86 layout_data = layout_data['layout']
87 sorting_hat = list()
88 for index, data in enumerate(layout_data):
89 sorting_hat.append([index, data['matrix']])
90
91 sorting_hat.sort(key=lambda k: (k[1][0], k[1][1]))
92
93 pos = 0
94 for row_num in range(0, keyboard_data['matrix_size']['rows']):
95 for col_num in range(0, keyboard_data['matrix_size']['cols']):
96 if pos >= len(sorting_hat) or sorting_hat[pos][1][0] != row_num or sorting_hat[pos][1][1] != col_num:
97 sorting_hat.insert(pos, [None, [row_num, col_num]])
98 else:
99 sorting_hat.append([None, [row_num, col_num]])
100 pos += 1
101
102 keymap_data = list()
103 for layer in via_backup['layers']:
104 pos = 0
105 layer_data = list()
106 for key in layer:
107 if sorting_hat[pos][0] is not None:
108 layer_data.append([sorting_hat[pos][0], key])
109 pos += 1
110 layer_data.sort()
111 layer_data = [kc[1] for kc in layer_data]
112 keymap_data.append(layer_data)
113
114 return keymap_data
115
116
117@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
118@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
119@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='VIA Backup JSON file')
120@cli.argument('-kb', '--keyboard', type=qmk.keyboard.keyboard_folder, completer=qmk.keyboard.keyboard_completer, arg_only=True, required=True, help='The keyboard\'s name')
121@cli.argument('-km', '--keymap', arg_only=True, default='via2json', help='The keymap\'s name')
122@cli.argument('-l', '--layout', arg_only=True, help='The keymap\'s layout')
123@cli.subcommand('Convert a VIA backup json to keymap.json format.')
124def via2json(cli):
125 """Convert a VIA backup json to keymap.json format.
126
127 This command uses the `qmk.keymap` module to generate a keymap.json from a VIA backup json. The generated keymap is written to stdout, or to a file if -o is provided.
128 """
129 # Load the VIA backup json
130 with cli.args.filename.open('r') as fd:
131 via_backup = json.load(fd)
132
133 keyboard_data = info_json(cli.args.keyboard)
134
135 # Find appropriate layout macro
136 keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(keyboard_data)
137 if not keymap_layout:
138 cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.")
139 return False
140
141 # Get keycode array
142 keymap_data = _via_to_keymap(via_backup, keyboard_data, keymap_layout)
143 if not keymap_data:
144 cli.log.error(f'Could not extract valid keycode data from VIA backup matching keyboard {cli.args.keyboard}!')
145 return False
146
147 # Convert macros
148 macro_data = list()
149 if via_backup.get('macros'):
150 macro_data = _convert_macros(via_backup['macros'])
151
152 # Replace VIA macro keys with JSON keymap ones
153 keymap_data = _fix_macro_keys(keymap_data)
154
155 # Generate the keymap.json
156 keymap_json = generate_json(cli.args.keymap, cli.args.keyboard, keymap_layout, keymap_data, macro_data)
157
158 keymap_lines = [json.dumps(keymap_json, cls=KeymapJSONEncoder, sort_keys=True)]
159 dump_lines(cli.args.output, keymap_lines, cli.args.quiet)