keyboard stuff
1"""Functions that help us work with keyboards.
2"""
3from array import array
4from functools import lru_cache
5from math import ceil
6from pathlib import Path
7import os
8from glob import glob
9
10import qmk.path
11from qmk.c_parse import parse_config_h_file
12from qmk.json_schema import json_load
13from qmk.makefile import parse_rules_mk_file
14
15BOX_DRAWING_CHARACTERS = {
16 "unicode": {
17 "tl": "┌",
18 "tr": "┐",
19 "bl": "└",
20 "br": "┘",
21 "v": "│",
22 "h": "─",
23 },
24 "ascii": {
25 "tl": " ",
26 "tr": " ",
27 "bl": "|",
28 "br": "|",
29 "v": "|",
30 "h": "_",
31 },
32}
33ENC_DRAWING_CHARACTERS = {
34 "unicode": {
35 "tl": "╭",
36 "tr": "╮",
37 "bl": "╰",
38 "br": "╯",
39 "vl": "▲",
40 "vr": "▼",
41 "v": "│",
42 "h": "─",
43 },
44 "ascii": {
45 "tl": " ",
46 "tr": " ",
47 "bl": "\\",
48 "br": "/",
49 "v": "|",
50 "vl": "/",
51 "vr": "\\",
52 "h": "_",
53 },
54}
55
56
57class AllKeyboards:
58 """Represents all keyboards.
59 """
60 def __str__(self):
61 return 'all'
62
63 def __repr__(self):
64 return 'all'
65
66 def __eq__(self, other):
67 return isinstance(other, AllKeyboards)
68
69
70base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
71
72
73@lru_cache(maxsize=1)
74def keyboard_alias_definitions():
75 return json_load(Path('data/mappings/keyboard_aliases.hjson'))
76
77
78def is_all_keyboards(keyboard):
79 """Returns True if the keyboard is an AllKeyboards object.
80 """
81 if isinstance(keyboard, str):
82 return (keyboard == 'all')
83 return isinstance(keyboard, AllKeyboards)
84
85
86def find_keyboard_from_dir():
87 """Returns a keyboard name based on the user's current directory.
88 """
89 relative_cwd = qmk.path.under_qmk_userspace()
90 if not relative_cwd:
91 relative_cwd = qmk.path.under_qmk_firmware()
92
93 if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards':
94 # Attempt to extract the keyboard name from the current directory
95 current_path = Path('/'.join(relative_cwd.parts[1:]))
96
97 if 'keymaps' in current_path.parts:
98 # Strip current_path of anything after `keymaps`
99 keymap_index = len(current_path.parts) - current_path.parts.index('keymaps') - 1
100 current_path = current_path.parents[keymap_index]
101
102 if qmk.path.is_keyboard(current_path):
103 return str(current_path)
104
105
106def find_readme(keyboard):
107 """Returns the readme for this keyboard.
108 """
109 cur_dir = qmk.path.keyboard(keyboard)
110 keyboards_dir = Path('keyboards')
111 while not (cur_dir / 'readme.md').exists():
112 if cur_dir == keyboards_dir:
113 return None
114 cur_dir = cur_dir.parent
115
116 return cur_dir / 'readme.md'
117
118
119def keyboard_folder(keyboard):
120 """Returns the actual keyboard folder.
121
122 This checks aliases to resolve the actual path for a keyboard.
123 """
124 aliases = keyboard_alias_definitions()
125
126 while keyboard in aliases:
127 last_keyboard = keyboard
128 keyboard = aliases[keyboard].get('target', keyboard)
129 if keyboard == last_keyboard:
130 break
131
132 if not qmk.path.is_keyboard(keyboard):
133 raise ValueError(f'Invalid keyboard: {keyboard}')
134
135 return keyboard
136
137
138def keyboard_aliases(keyboard):
139 """Returns the list of aliases for the supplied keyboard.
140
141 Includes the keyboard itself.
142 """
143 aliases = json_load(Path('data/mappings/keyboard_aliases.hjson'))
144
145 if keyboard in aliases:
146 keyboard = aliases[keyboard].get('target', keyboard)
147
148 keyboards = set(filter(lambda k: aliases[k].get('target', '') == keyboard, aliases.keys()))
149 keyboards.add(keyboard)
150 keyboards = list(sorted(keyboards))
151 return keyboards
152
153
154def keyboard_folder_or_all(keyboard):
155 """Returns the actual keyboard folder.
156
157 This checks aliases to resolve the actual path for a keyboard.
158 If the supplied argument is "all", it returns an AllKeyboards object.
159 """
160 if keyboard == 'all':
161 return AllKeyboards()
162
163 return keyboard_folder(keyboard)
164
165
166def _find_name(path):
167 """Determine the keyboard name by stripping off the base_path and filename.
168 """
169 return path.replace(base_path, "").rsplit(os.path.sep, 1)[0]
170
171
172def keyboard_completer(prefix, action, parser, parsed_args):
173 """Returns a list of keyboards for tab completion.
174 """
175 return list_keyboards()
176
177
178@lru_cache(maxsize=None)
179def list_keyboards():
180 """Returns a list of all keyboards.
181 """
182 # We avoid pathlib here because this is performance critical code.
183 kb_wildcard = os.path.join(base_path, "**", 'keyboard.json')
184 paths = [path for path in glob(kb_wildcard, recursive=True) if os.path.sep + 'keymaps' + os.path.sep not in path]
185
186 found = map(_find_name, paths)
187
188 # Convert to posix paths for consistency
189 found = map(lambda x: str(Path(x).as_posix()), found)
190
191 return sorted(set(found))
192
193
194def config_h(keyboard):
195 """Parses all the config.h files for a keyboard.
196
197 Args:
198 keyboard: name of the keyboard
199
200 Returns:
201 a dictionary representing the content of the entire config.h tree for a keyboard
202 """
203 config = {}
204 cur_dir = Path('keyboards')
205 keyboard = Path(keyboard)
206
207 for dir in keyboard.parts:
208 cur_dir = cur_dir / dir
209 config = {**config, **parse_config_h_file(cur_dir / 'config.h')}
210
211 return config
212
213
214def rules_mk(keyboard):
215 """Get a rules.mk for a keyboard
216
217 Args:
218 keyboard: name of the keyboard
219
220 Returns:
221 a dictionary representing the content of the entire rules.mk tree for a keyboard
222 """
223 cur_dir = Path('keyboards')
224 keyboard = Path(keyboard)
225 rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
226
227 for i, dir in enumerate(keyboard.parts):
228 cur_dir = cur_dir / dir
229 rules = parse_rules_mk_file(cur_dir / 'rules.mk', rules)
230
231 return rules
232
233
234def render_layout(layout_data, render_ascii, key_labels=None):
235 """Renders a single layout.
236 """
237 textpad = [array('u', ' ' * 200) for x in range(100)]
238 style = 'ascii' if render_ascii else 'unicode'
239
240 for key in layout_data:
241 x = key.get('x', 0)
242 y = key.get('y', 0)
243 w = key.get('w', 1)
244 h = key.get('h', 1)
245
246 if key_labels:
247 label = key_labels.pop(0)
248 if label.startswith('KC_'):
249 label = label[3:]
250 else:
251 label = key.get('label', '')
252
253 if 'encoder' in key:
254 render_encoder(textpad, x, y, w, h, label, style)
255 elif x >= 0.25 and w == 1.25 and h == 2:
256 render_key_isoenter(textpad, x, y, w, h, label, style)
257 elif w == 1.5 and h == 2:
258 render_key_baenter(textpad, x, y, w, h, label, style)
259 else:
260 render_key_rect(textpad, x, y, w, h, label, style)
261
262 lines = []
263 for line in textpad:
264 if line.tounicode().strip():
265 lines.append(line.tounicode().rstrip())
266
267 return '\n'.join(lines)
268
269
270def render_layouts(info_json, render_ascii):
271 """Renders all the layouts from an `info_json` structure.
272 """
273 layouts = {}
274
275 for layout in info_json['layouts']:
276 layout_data = info_json['layouts'][layout]['layout']
277 layouts[layout] = render_layout(layout_data, render_ascii)
278
279 return layouts
280
281
282def render_key_rect(textpad, x, y, w, h, label, style):
283 box_chars = BOX_DRAWING_CHARACTERS[style]
284 x = ceil(x * 4)
285 y = ceil(y * 3)
286 w = ceil(w * 4)
287 h = ceil(h * 3)
288
289 label_len = w - 2
290 label_leftover = label_len - len(label)
291
292 if len(label) > label_len:
293 label = label[:label_len]
294
295 label_blank = ' ' * label_len
296 label_border = box_chars['h'] * label_len
297 label_middle = label + ' ' * label_leftover
298
299 top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
300 lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
301 mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
302 bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
303
304 textpad[y][x:x + w] = top_line
305 textpad[y + 1][x:x + w] = lab_line
306 for i in range(h - 3):
307 textpad[y + i + 2][x:x + w] = mid_line
308 textpad[y + h - 1][x:x + w] = bot_line
309
310
311def render_key_isoenter(textpad, x, y, w, h, label, style):
312 box_chars = BOX_DRAWING_CHARACTERS[style]
313 x = ceil(x * 4)
314 y = ceil(y * 3)
315 w = ceil(w * 4)
316 h = ceil(h * 3)
317
318 label_len = w - 1
319 label_leftover = label_len - len(label)
320
321 if len(label) > label_len:
322 label = label[:label_len]
323
324 label_blank = ' ' * (label_len - 1)
325 label_border_top = box_chars['h'] * label_len
326 label_border_bottom = box_chars['h'] * (label_len - 1)
327 label_middle = label + ' ' * label_leftover
328
329 top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
330 lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
331 crn_line = array('u', box_chars['bl'] + box_chars['tr'] + label_blank + box_chars['v'])
332 mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
333 bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
334
335 textpad[y][x - 1:x + w] = top_line
336 textpad[y + 1][x - 1:x + w] = lab_line
337 textpad[y + 2][x - 1:x + w] = crn_line
338 textpad[y + 3][x:x + w] = mid_line
339 textpad[y + 4][x:x + w] = mid_line
340 textpad[y + 5][x:x + w] = bot_line
341
342
343def render_key_baenter(textpad, x, y, w, h, label, style):
344 box_chars = BOX_DRAWING_CHARACTERS[style]
345 x = ceil(x * 4)
346 y = ceil(y * 3)
347 w = ceil(w * 4)
348 h = ceil(h * 3)
349
350 label_len = w + 1
351 label_leftover = label_len - len(label)
352
353 if len(label) > label_len:
354 label = label[:label_len]
355
356 label_blank = ' ' * (label_len - 3)
357 label_border_top = box_chars['h'] * (label_len - 3)
358 label_border_bottom = box_chars['h'] * label_len
359 label_middle = label + ' ' * label_leftover
360
361 top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
362 mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
363 crn_line = array('u', box_chars['tl'] + box_chars['h'] + box_chars['h'] + box_chars['br'] + label_blank + box_chars['v'])
364 lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
365 bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
366
367 textpad[y][x:x + w] = top_line
368 textpad[y + 1][x:x + w] = mid_line
369 textpad[y + 2][x:x + w] = mid_line
370 textpad[y + 3][x - 3:x + w] = crn_line
371 textpad[y + 4][x - 3:x + w] = lab_line
372 textpad[y + 5][x - 3:x + w] = bot_line
373
374
375def render_encoder(textpad, x, y, w, h, label, style):
376 box_chars = ENC_DRAWING_CHARACTERS[style]
377 x = ceil(x * 4)
378 y = ceil(y * 3)
379 w = ceil(w * 4)
380 h = ceil(h * 3)
381
382 label_len = w - 2
383 label_leftover = label_len - len(label)
384
385 if len(label) > label_len:
386 label = label[:label_len]
387
388 label_blank = ' ' * label_len
389 label_border = box_chars['h'] * label_len
390 label_middle = label + ' ' * label_leftover
391
392 top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
393 lab_line = array('u', box_chars['vl'] + label_middle + box_chars['vr'])
394 mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
395 bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
396
397 textpad[y][x:x + w] = top_line
398 textpad[y + 1][x:x + w] = lab_line
399 for i in range(h - 3):
400 textpad[y + i + 2][x:x + w] = mid_line
401 textpad[y + h - 1][x:x + w] = bot_line