keyboard.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. """Functions that help us work with keyboards.
  2. """
  3. from array import array
  4. from math import ceil
  5. from pathlib import Path
  6. import os
  7. import shutil
  8. from glob import glob
  9. import qmk.path
  10. from qmk.c_parse import parse_config_h_file
  11. from qmk.json_schema import json_load
  12. from qmk.makefile import parse_rules_mk_file
  13. KEY_WIDTH = 4 if shutil.get_terminal_size().columns < 160 else 6
  14. BOX_DRAWING_CHARACTERS = {
  15. "unicode": {
  16. "tl": "┌",
  17. "tr": "┐",
  18. "bl": "└",
  19. "br": "┘",
  20. "v": "│",
  21. "h": "─",
  22. },
  23. "ascii": {
  24. "tl": " ",
  25. "tr": " ",
  26. "bl": "|",
  27. "br": "|",
  28. "v": "|",
  29. "h": "_",
  30. },
  31. }
  32. base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
  33. def find_keyboard_from_dir():
  34. """Returns a keyboard name based on the user's current directory.
  35. """
  36. relative_cwd = qmk.path.under_qmk_firmware()
  37. if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards':
  38. # Attempt to extract the keyboard name from the current directory
  39. current_path = Path('/'.join(relative_cwd.parts[1:]))
  40. if 'keymaps' in current_path.parts:
  41. # Strip current_path of anything after `keymaps`
  42. keymap_index = len(current_path.parts) - current_path.parts.index('keymaps') - 1
  43. current_path = current_path.parents[keymap_index]
  44. if qmk.path.is_keyboard(current_path):
  45. return str(current_path)
  46. def find_readme(keyboard):
  47. """Returns the readme for this keyboard.
  48. """
  49. cur_dir = qmk.path.keyboard(keyboard)
  50. keyboards_dir = Path('keyboards')
  51. while not (cur_dir / 'readme.md').exists():
  52. if cur_dir == keyboards_dir:
  53. return None
  54. cur_dir = cur_dir.parent
  55. return cur_dir / 'readme.md'
  56. def keyboard_folder(keyboard):
  57. """Returns the actual keyboard folder.
  58. This checks aliases and DEFAULT_FOLDER to resolve the actual path for a keyboard.
  59. """
  60. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  61. if keyboard in aliases:
  62. keyboard = aliases[keyboard].get('target', keyboard)
  63. rules_mk_file = Path(base_path, keyboard, 'rules.mk')
  64. if rules_mk_file.exists():
  65. rules_mk = parse_rules_mk_file(rules_mk_file)
  66. keyboard = rules_mk.get('DEFAULT_FOLDER', keyboard)
  67. if not qmk.path.is_keyboard(keyboard):
  68. raise ValueError(f'Invalid keyboard: {keyboard}')
  69. return keyboard
  70. def _find_name(path):
  71. """Determine the keyboard name by stripping off the base_path and rules.mk.
  72. """
  73. return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
  74. def keyboard_completer(prefix, action, parser, parsed_args):
  75. """Returns a list of keyboards for tab completion.
  76. """
  77. return list_keyboards()
  78. def list_keyboards():
  79. """Returns a list of all keyboards.
  80. """
  81. # We avoid pathlib here because this is performance critical code.
  82. kb_wildcard = os.path.join(base_path, "**", "rules.mk")
  83. paths = [path for path in glob(kb_wildcard, recursive=True) if os.path.sep + 'keymaps' + os.path.sep not in path]
  84. return sorted(set(map(resolve_keyboard, map(_find_name, paths))))
  85. def resolve_keyboard(keyboard):
  86. cur_dir = Path('keyboards')
  87. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  88. while 'DEFAULT_FOLDER' in rules and keyboard != rules['DEFAULT_FOLDER']:
  89. keyboard = rules['DEFAULT_FOLDER']
  90. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  91. return keyboard
  92. def config_h(keyboard):
  93. """Parses all the config.h files for a keyboard.
  94. Args:
  95. keyboard: name of the keyboard
  96. Returns:
  97. a dictionary representing the content of the entire config.h tree for a keyboard
  98. """
  99. config = {}
  100. cur_dir = Path('keyboards')
  101. keyboard = Path(resolve_keyboard(keyboard))
  102. for dir in keyboard.parts:
  103. cur_dir = cur_dir / dir
  104. config = {**config, **parse_config_h_file(cur_dir / 'config.h')}
  105. return config
  106. def rules_mk(keyboard):
  107. """Get a rules.mk for a keyboard
  108. Args:
  109. keyboard: name of the keyboard
  110. Returns:
  111. a dictionary representing the content of the entire rules.mk tree for a keyboard
  112. """
  113. cur_dir = Path('keyboards')
  114. keyboard = Path(resolve_keyboard(keyboard))
  115. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  116. for i, dir in enumerate(keyboard.parts):
  117. cur_dir = cur_dir / dir
  118. rules = parse_rules_mk_file(cur_dir / 'rules.mk', rules)
  119. return rules
  120. def render_layout(layout_data, render_ascii, key_labels=None):
  121. """Renders a single layout.
  122. """
  123. textpad = [array('u', ' ' * 200) for x in range(100)]
  124. style = 'ascii' if render_ascii else 'unicode'
  125. for key in layout_data:
  126. x = key.get('x', 0)
  127. y = key.get('y', 0)
  128. w = key.get('w', 1)
  129. h = key.get('h', 1)
  130. if key_labels:
  131. label = key_labels.pop(0)
  132. if label.startswith('KC_'):
  133. label = label[3:]
  134. else:
  135. label = key.get('label', '')
  136. if x >= 0.25 and w == 1.25 and h == 2:
  137. render_key_isoenter(textpad, x, y, w, h, label, style)
  138. elif w == 2.25 and h == 2:
  139. render_key_baenter(textpad, x, y, w, h, label, style)
  140. else:
  141. render_key_rect(textpad, x, y, w, h, label, style)
  142. lines = []
  143. for line in textpad:
  144. if line.tounicode().strip():
  145. lines.append(line.tounicode().rstrip())
  146. return '\n'.join(lines)
  147. def render_layouts(info_json, render_ascii):
  148. """Renders all the layouts from an `info_json` structure.
  149. """
  150. layouts = {}
  151. for layout in info_json['layouts']:
  152. layout_data = info_json['layouts'][layout]['layout']
  153. layouts[layout] = render_layout(layout_data, render_ascii)
  154. return layouts
  155. def render_key_rect(textpad, x, y, w, h, label, style):
  156. box_chars = BOX_DRAWING_CHARACTERS[style]
  157. x = ceil(x * KEY_WIDTH)
  158. y = ceil(y * 3)
  159. w = ceil(w * KEY_WIDTH)
  160. h = ceil(h * 3)
  161. label_len = w - 2
  162. label_leftover = label_len - len(label)
  163. if len(label) > label_len:
  164. label = label[:label_len]
  165. label_blank = ' ' * label_len
  166. label_border = box_chars['h'] * label_len
  167. label_middle = label + ' ' * label_leftover
  168. top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
  169. lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
  170. mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
  171. bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
  172. textpad[y][x:x + w] = top_line
  173. textpad[y + 1][x:x + w] = lab_line
  174. for i in range(h - 3):
  175. textpad[y + i + 2][x:x + w] = mid_line
  176. textpad[y + h - 1][x:x + w] = bot_line
  177. def render_key_isoenter(textpad, x, y, w, h, label, style):
  178. box_chars = BOX_DRAWING_CHARACTERS[style]
  179. x = ceil(x * KEY_WIDTH)
  180. y = ceil(y * 3)
  181. w = ceil(w * KEY_WIDTH)
  182. h = ceil(h * 3)
  183. label_len = w - 1
  184. label_leftover = label_len - len(label)
  185. if len(label) > label_len:
  186. label = label[:label_len]
  187. label_blank = ' ' * (label_len - 1)
  188. label_border_top = box_chars['h'] * label_len
  189. label_border_bottom = box_chars['h'] * (label_len - 1)
  190. label_middle = label + ' ' * label_leftover
  191. top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
  192. lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
  193. crn_line = array('u', box_chars['bl'] + box_chars['tr'] + label_blank + box_chars['v'])
  194. mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
  195. bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
  196. textpad[y][x - 1:x + w] = top_line
  197. textpad[y + 1][x - 1:x + w] = lab_line
  198. textpad[y + 2][x - 1:x + w] = crn_line
  199. textpad[y + 3][x:x + w] = mid_line
  200. textpad[y + 4][x:x + w] = mid_line
  201. textpad[y + 5][x:x + w] = bot_line
  202. def render_key_baenter(textpad, x, y, w, h, label, style):
  203. box_chars = BOX_DRAWING_CHARACTERS[style]
  204. x = ceil(x * KEY_WIDTH)
  205. y = ceil(y * 3)
  206. w = ceil(w * KEY_WIDTH)
  207. h = ceil(h * 3)
  208. label_len = w - 2
  209. label_leftover = label_len - len(label)
  210. if len(label) > label_len:
  211. label = label[:label_len]
  212. label_blank = ' ' * (label_len - 3)
  213. label_border_top = box_chars['h'] * (label_len - 3)
  214. label_border_bottom = box_chars['h'] * label_len
  215. label_middle = label + ' ' * label_leftover
  216. top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
  217. mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
  218. crn_line = array('u', box_chars['tl'] + box_chars['h'] + box_chars['h'] + box_chars['br'] + label_blank + box_chars['v'])
  219. lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
  220. bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
  221. textpad[y][x + 3:x + w] = top_line
  222. textpad[y + 1][x + 3:x + w] = mid_line
  223. textpad[y + 2][x + 3:x + w] = mid_line
  224. textpad[y + 3][x:x + w] = crn_line
  225. textpad[y + 4][x:x + w] = lab_line
  226. textpad[y + 5][x:x + w] = bot_line