You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
9.9 KiB

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