qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

keyboard.py (11634B)


      1 """Functions that help us work with keyboards.
      2 """
      3 from array import array
      4 from functools import lru_cache
      5 from math import ceil
      6 from pathlib import Path
      7 import os
      8 from glob import glob
      9 
     10 import qmk.path
     11 from qmk.c_parse import parse_config_h_file
     12 from qmk.json_schema import json_load
     13 from qmk.makefile import parse_rules_mk_file
     14 
     15 BOX_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 }
     33 ENC_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 
     57 class 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 
     70 base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
     71 
     72 
     73 @lru_cache(maxsize=1)
     74 def keyboard_alias_definitions():
     75     return json_load(Path('data/mappings/keyboard_aliases.hjson'))
     76 
     77 
     78 def 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 
     86 def 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 
    106 def 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 
    119 def 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 
    138 def 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 
    154 def 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 
    166 def _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 
    172 def 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)
    179 def 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 
    194 def 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 
    214 def 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 
    234 def 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 
    270 def 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 
    282 def 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 
    311 def 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 
    343 def 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 
    375 def 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