logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

keyboard.py (12240B)


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