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.

229 lines
6.6 KiB

  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. from glob import glob
  8. import qmk.path
  9. from qmk.c_parse import parse_config_h_file
  10. from qmk.json_schema import json_load
  11. from qmk.makefile import parse_rules_mk_file
  12. BOX_DRAWING_CHARACTERS = {
  13. "unicode": {
  14. "tl": "",
  15. "tr": "",
  16. "bl": "",
  17. "br": "",
  18. "v": "",
  19. "h": "",
  20. },
  21. "ascii": {
  22. "tl": " ",
  23. "tr": " ",
  24. "bl": "|",
  25. "br": "|",
  26. "v": "|",
  27. "h": "_",
  28. },
  29. }
  30. base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
  31. def find_keyboard_from_dir():
  32. """Returns a keyboard name based on the user's current directory.
  33. """
  34. relative_cwd = qmk.path.under_qmk_firmware()
  35. if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards':
  36. # Attempt to extract the keyboard name from the current directory
  37. current_path = Path('/'.join(relative_cwd.parts[1:]))
  38. if 'keymaps' in current_path.parts:
  39. # Strip current_path of anything after `keymaps`
  40. keymap_index = len(current_path.parts) - current_path.parts.index('keymaps') - 1
  41. current_path = current_path.parents[keymap_index]
  42. if qmk.path.is_keyboard(current_path):
  43. return str(current_path)
  44. def find_readme(keyboard):
  45. """Returns the readme for this keyboard.
  46. """
  47. cur_dir = qmk.path.keyboard(keyboard)
  48. keyboards_dir = Path('keyboards')
  49. while not (cur_dir / 'readme.md').exists():
  50. if cur_dir == keyboards_dir:
  51. return None
  52. cur_dir = cur_dir.parent
  53. return cur_dir / 'readme.md'
  54. def is_keyboard_target(keyboard_target):
  55. """Checks to make sure the supplied keyboard_target is valid.
  56. This is mainly used by commands that accept --keyboard.
  57. """
  58. if keyboard_target in ['all', 'all-avr', 'all-chibios', 'all-arm_atsam']:
  59. return keyboard_target
  60. return keyboard_folder(keyboard_target)
  61. def keyboard_folder(keyboard):
  62. """Returns the actual keyboard folder.
  63. This checks aliases and DEFAULT_FOLDER to resolve the actual path for a keyboard.
  64. """
  65. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  66. if keyboard in aliases:
  67. keyboard = aliases[keyboard].get('target', keyboard)
  68. rules_mk_file = Path(base_path, keyboard, 'rules.mk')
  69. if rules_mk_file.exists():
  70. rules_mk = parse_rules_mk_file(rules_mk_file)
  71. keyboard = rules_mk.get('DEFAULT_FOLDER', keyboard)
  72. if not qmk.path.is_keyboard(keyboard):
  73. raise ValueError(f'Invalid keyboard: {keyboard}')
  74. return keyboard
  75. def _find_name(path):
  76. """Determine the keyboard name by stripping off the base_path and rules.mk.
  77. """
  78. return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
  79. def keyboard_completer(prefix, action, parser, parsed_args):
  80. """Returns a list of keyboards for tab completion.
  81. """
  82. return list_keyboards()
  83. def list_keyboards():
  84. """Returns a list of all keyboards.
  85. """
  86. # We avoid pathlib here because this is performance critical code.
  87. kb_wildcard = os.path.join(base_path, "**", "rules.mk")
  88. paths = [path for path in glob(kb_wildcard, recursive=True) if 'keymaps' not in path]
  89. return sorted(set(map(resolve_keyboard, map(_find_name, paths))))
  90. def resolve_keyboard(keyboard):
  91. cur_dir = Path('keyboards')
  92. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  93. while 'DEFAULT_FOLDER' in rules and keyboard != rules['DEFAULT_FOLDER']:
  94. keyboard = rules['DEFAULT_FOLDER']
  95. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  96. return keyboard
  97. def config_h(keyboard):
  98. """Parses all the config.h files for a keyboard.
  99. Args:
  100. keyboard: name of the keyboard
  101. Returns:
  102. a dictionary representing the content of the entire config.h tree for a keyboard
  103. """
  104. config = {}
  105. cur_dir = Path('keyboards')
  106. keyboard = Path(resolve_keyboard(keyboard))
  107. for dir in keyboard.parts:
  108. cur_dir = cur_dir / dir
  109. config = {**config, **parse_config_h_file(cur_dir / 'config.h')}
  110. return config
  111. def rules_mk(keyboard):
  112. """Get a rules.mk for a keyboard
  113. Args:
  114. keyboard: name of the keyboard
  115. Returns:
  116. a dictionary representing the content of the entire rules.mk tree for a keyboard
  117. """
  118. cur_dir = Path('keyboards')
  119. keyboard = Path(resolve_keyboard(keyboard))
  120. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  121. for i, dir in enumerate(keyboard.parts):
  122. cur_dir = cur_dir / dir
  123. rules = parse_rules_mk_file(cur_dir / 'rules.mk', rules)
  124. return rules
  125. def render_layout(layout_data, render_ascii, key_labels=None):
  126. """Renders a single layout.
  127. """
  128. textpad = [array('u', ' ' * 200) for x in range(100)]
  129. style = 'ascii' if render_ascii else 'unicode'
  130. box_chars = BOX_DRAWING_CHARACTERS[style]
  131. for key in layout_data:
  132. x = ceil(key.get('x', 0) * 4)
  133. y = ceil(key.get('y', 0) * 3)
  134. w = ceil(key.get('w', 1) * 4)
  135. h = ceil(key.get('h', 1) * 3)
  136. if key_labels:
  137. label = key_labels.pop(0)
  138. if label.startswith('KC_'):
  139. label = label[3:]
  140. else:
  141. label = key.get('label', '')
  142. label_len = w - 2
  143. label_leftover = label_len - len(label)
  144. if len(label) > label_len:
  145. label = label[:label_len]
  146. label_blank = ' ' * label_len
  147. label_border = box_chars['h'] * label_len
  148. label_middle = label + ' '*label_leftover # noqa: yapf insists there be no whitespace around *
  149. top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
  150. lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
  151. mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
  152. bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
  153. textpad[y][x:x + w] = top_line
  154. textpad[y + 1][x:x + w] = lab_line
  155. for i in range(h - 3):
  156. textpad[y + i + 2][x:x + w] = mid_line
  157. textpad[y + h - 1][x:x + w] = bot_line
  158. lines = []
  159. for line in textpad:
  160. if line.tounicode().strip():
  161. lines.append(line.tounicode().rstrip())
  162. return '\n'.join(lines)
  163. def render_layouts(info_json, render_ascii):
  164. """Renders all the layouts from an `info_json` structure.
  165. """
  166. layouts = {}
  167. for layout in info_json['layouts']:
  168. layout_data = info_json['layouts'][layout]['layout']
  169. layouts[layout] = render_layout(layout_data, render_ascii)
  170. return layouts