keyboard.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. from qmk.c_parse import parse_config_h_file
  9. from qmk.json_schema import json_load
  10. from qmk.makefile import parse_rules_mk_file
  11. from qmk.path import is_keyboard
  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 keyboard_folder(keyboard):
  32. """Returns the actual keyboard folder.
  33. This checks aliases and DEFAULT_FOLDER to resolve the actual path for a keyboard.
  34. """
  35. aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
  36. if keyboard in aliases:
  37. keyboard = aliases[keyboard].get('target', keyboard)
  38. rules_mk_file = Path(base_path, keyboard, 'rules.mk')
  39. if rules_mk_file.exists():
  40. rules_mk = parse_rules_mk_file(rules_mk_file)
  41. keyboard = rules_mk.get('DEFAULT_FOLDER', keyboard)
  42. if not is_keyboard(keyboard):
  43. raise ValueError(f'Invalid keyboard: {keyboard}')
  44. return keyboard
  45. def _find_name(path):
  46. """Determine the keyboard name by stripping off the base_path and rules.mk.
  47. """
  48. return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
  49. def list_keyboards():
  50. """Returns a list of all keyboards.
  51. """
  52. # We avoid pathlib here because this is performance critical code.
  53. kb_wildcard = os.path.join(base_path, "**", "rules.mk")
  54. paths = [path for path in glob(kb_wildcard, recursive=True) if 'keymaps' not in path]
  55. return sorted(map(_find_name, paths))
  56. def config_h(keyboard):
  57. """Parses all the config.h files for a keyboard.
  58. Args:
  59. keyboard: name of the keyboard
  60. Returns:
  61. a dictionary representing the content of the entire config.h tree for a keyboard
  62. """
  63. config = {}
  64. cur_dir = Path('keyboards')
  65. rules = rules_mk(keyboard)
  66. keyboard = Path(rules['DEFAULT_FOLDER'] if 'DEFAULT_FOLDER' in rules else keyboard)
  67. for dir in keyboard.parts:
  68. cur_dir = cur_dir / dir
  69. config = {**config, **parse_config_h_file(cur_dir / 'config.h')}
  70. return config
  71. def rules_mk(keyboard):
  72. """Get a rules.mk for a keyboard
  73. Args:
  74. keyboard: name of the keyboard
  75. Returns:
  76. a dictionary representing the content of the entire rules.mk tree for a keyboard
  77. """
  78. keyboard = Path(keyboard)
  79. cur_dir = Path('keyboards')
  80. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  81. if 'DEFAULT_FOLDER' in rules:
  82. keyboard = Path(rules['DEFAULT_FOLDER'])
  83. for i, dir in enumerate(keyboard.parts):
  84. cur_dir = cur_dir / dir
  85. rules = parse_rules_mk_file(cur_dir / 'rules.mk', rules)
  86. return rules
  87. def render_layout(layout_data, render_ascii, key_labels=None):
  88. """Renders a single layout.
  89. """
  90. textpad = [array('u', ' ' * 200) for x in range(50)]
  91. style = 'ascii' if render_ascii else 'unicode'
  92. box_chars = BOX_DRAWING_CHARACTERS[style]
  93. for key in layout_data:
  94. x = ceil(key.get('x', 0) * 4)
  95. y = ceil(key.get('y', 0) * 3)
  96. w = ceil(key.get('w', 1) * 4)
  97. h = ceil(key.get('h', 1) * 3)
  98. if key_labels:
  99. label = key_labels.pop(0)
  100. if label.startswith('KC_'):
  101. label = label[3:]
  102. else:
  103. label = key.get('label', '')
  104. label_len = w - 2
  105. label_leftover = label_len - len(label)
  106. if len(label) > label_len:
  107. label = label[:label_len]
  108. label_blank = ' ' * label_len
  109. label_border = box_chars['h'] * label_len
  110. label_middle = label + ' '*label_leftover # noqa: yapf insists there be no whitespace around *
  111. top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
  112. lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
  113. mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
  114. bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
  115. textpad[y][x:x + w] = top_line
  116. textpad[y + 1][x:x + w] = lab_line
  117. for i in range(h - 3):
  118. textpad[y + i + 2][x:x + w] = mid_line
  119. textpad[y + h - 1][x:x + w] = bot_line
  120. lines = []
  121. for line in textpad:
  122. if line.tounicode().strip():
  123. lines.append(line.tounicode().rstrip())
  124. return '\n'.join(lines)
  125. def render_layouts(info_json, render_ascii):
  126. """Renders all the layouts from an `info_json` structure.
  127. """
  128. layouts = {}
  129. for layout in info_json['layouts']:
  130. layout_data = info_json['layouts'][layout]['layout']
  131. layouts[layout] = render_layout(layout_data, render_ascii)
  132. return layouts