at master 12 kB view raw
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