info.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. """Keyboard information script.
  2. Compile an info.json for a particular keyboard and pretty-print it.
  3. """
  4. import sys
  5. import json
  6. from milc import cli
  7. from qmk.json_encoders import InfoJSONEncoder
  8. from qmk.constants import COL_LETTERS, ROW_LETTERS
  9. from qmk.decorators import automagic_keyboard, automagic_keymap
  10. from qmk.keyboard import keyboard_completer, keyboard_folder, render_layouts, render_layout, rules_mk
  11. from qmk.info import info_json, keymap_json
  12. from qmk.keymap import locate_keymap
  13. from qmk.path import is_keyboard
  14. UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
  15. def _strip_api_content(info_json):
  16. # Ideally this would only be added in the API pathway.
  17. info_json.pop('platform', None)
  18. info_json.pop('platform_key', None)
  19. info_json.pop('processor_type', None)
  20. info_json.pop('protocol', None)
  21. info_json.pop('keymaps', None)
  22. info_json.pop('keyboard_folder', None)
  23. info_json.pop('parse_errors', None)
  24. info_json.pop('parse_warnings', None)
  25. for layout in info_json.get('layouts', {}).values():
  26. layout.pop('filename', None)
  27. layout.pop('c_macro', None)
  28. layout.pop('json_layout', None)
  29. if 'matrix_pins' in info_json:
  30. info_json.pop('matrix_size', None)
  31. for feature in ['rgb_matrix', 'led_matrix']:
  32. if info_json.get(feature, {}).get("layout", None):
  33. info_json[feature].pop('led_count', None)
  34. return info_json
  35. def show_keymap(kb_info_json, title_caps=True):
  36. """Render the keymap in ascii art.
  37. """
  38. keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
  39. if keymap_path and keymap_path.suffix == '.json':
  40. keymap_data = json.load(keymap_path.open(encoding='utf-8'))
  41. # cater for layout-less keymap.json
  42. if 'layout' not in keymap_data:
  43. return
  44. layout_name = keymap_data['layout']
  45. layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
  46. for layer_num, layer in enumerate(keymap_data['layers']):
  47. if title_caps:
  48. cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
  49. else:
  50. cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
  51. print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
  52. def show_layouts(kb_info_json, title_caps=True):
  53. """Render the layouts with info.json labels.
  54. """
  55. for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
  56. title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
  57. cli.echo('{fg_cyan}%s{fg_reset}:', title)
  58. print(layout_art) # Avoid passing dirty data to cli.echo()
  59. def show_matrix(kb_info_json, title_caps=True):
  60. """Render the layout with matrix labels in ascii art.
  61. """
  62. for layout_name, layout in kb_info_json['layouts'].items():
  63. # Build our label list
  64. labels = []
  65. for key in layout['layout']:
  66. if 'matrix' in key:
  67. row = ROW_LETTERS[key['matrix'][0]]
  68. col = COL_LETTERS[key['matrix'][1]]
  69. labels.append(row + col)
  70. else:
  71. labels.append('')
  72. # Print the header
  73. if title_caps:
  74. cli.echo('{fg_blue}Matrix for "%s"{fg_reset}:', layout_name)
  75. else:
  76. cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name)
  77. print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, labels))
  78. def show_leds(kb_info_json, title_caps=True):
  79. """Render LED indices per key, using the keyboard's key layout geometry.
  80. We build a map from (row, col) -> LED index using rgb_matrix/led_matrix layout,
  81. then label each key with its LED index. Keys without an associated LED are left blank.
  82. """
  83. # Prefer rgb_matrix, fall back to led_matrix
  84. led_feature = None
  85. for feature in ['rgb_matrix', 'led_matrix']:
  86. if 'layout' in kb_info_json.get(feature, {}):
  87. led_feature = feature
  88. break
  89. if not led_feature:
  90. cli.echo('{fg_yellow}No rgb_matrix/led_matrix layout found to derive LED indices.{fg_reset}')
  91. return
  92. # Build mapping from matrix position -> LED indices for faster lookup later
  93. by_matrix = {}
  94. for idx, led in enumerate(kb_info_json[led_feature]['layout']):
  95. if 'matrix' in led:
  96. led_key = tuple(led.get('matrix'))
  97. by_matrix[led_key] = idx
  98. # For each keyboard layout (e.g., LAYOUT), render keys labeled with LED index (or blank)
  99. for layout_name, layout in kb_info_json['layouts'].items():
  100. labels = []
  101. for key in layout['layout']:
  102. led_key = tuple(key.get('matrix'))
  103. label = str(by_matrix[led_key]) if led_key in by_matrix else ''
  104. labels.append(label)
  105. # Header
  106. if title_caps:
  107. cli.echo('{fg_blue}LED indices for "%s"{fg_reset}:', layout_name)
  108. else:
  109. cli.echo('{fg_blue}leds_%s{fg_reset}:', layout_name)
  110. print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, labels))
  111. def print_friendly_output(kb_info_json):
  112. """Print the info.json in a friendly text format.
  113. """
  114. cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', kb_info_json.get('keyboard_name', 'Unknown'))
  115. cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', kb_info_json.get('manufacturer', 'Unknown'))
  116. if 'url' in kb_info_json:
  117. cli.echo('{fg_blue}Website{fg_reset}: %s', kb_info_json.get('url', ''))
  118. if kb_info_json.get('maintainer', 'qmk') == 'qmk':
  119. cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community')
  120. else:
  121. cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
  122. cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
  123. cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
  124. cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
  125. if 'layout_aliases' in kb_info_json:
  126. aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
  127. cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
  128. def print_text_output(kb_info_json):
  129. """Print the info.json in a plain text format.
  130. """
  131. for key in sorted(kb_info_json):
  132. if key == 'layouts':
  133. cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
  134. else:
  135. cli.echo('{fg_blue}%s{fg_reset}: %s', key, kb_info_json[key])
  136. if cli.config.info.layouts:
  137. show_layouts(kb_info_json, False)
  138. if cli.config.info.matrix:
  139. show_matrix(kb_info_json, False)
  140. if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
  141. show_keymap(kb_info_json, False)
  142. def print_dotted_output(kb_info_json, prefix=''):
  143. """Print the info.json in a plain text format with dot-joined keys.
  144. """
  145. for key in sorted(kb_info_json):
  146. new_prefix = f'{prefix}.{key}' if prefix else key
  147. if key in ['parse_errors', 'parse_warnings']:
  148. continue
  149. elif key == 'layouts' and prefix == '':
  150. cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
  151. elif isinstance(kb_info_json[key], dict):
  152. print_dotted_output(kb_info_json[key], new_prefix)
  153. elif isinstance(kb_info_json[key], list):
  154. cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
  155. else:
  156. cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
  157. def print_parsed_rules_mk(keyboard_name):
  158. rules = rules_mk(keyboard_name)
  159. for k in sorted(rules.keys()):
  160. print('%s = %s' % (k, rules[k]))
  161. return
  162. @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
  163. @cli.argument('-km', '--keymap', help='Keymap to show info for (Optional).')
  164. @cli.argument('-l', '--layouts', action='store_true', help='Render the layouts.')
  165. @cli.argument('-m', '--matrix', action='store_true', help='Render the layouts with matrix information.')
  166. @cli.argument('-L', '--leds', action='store_true', help='Render the LED layout with LED indices (rgb_matrix/led_matrix).')
  167. @cli.argument('-f', '--format', default='friendly', arg_only=True, help='Format to display the data in (friendly, text, json) (Default: friendly).')
  168. @cli.argument('--ascii', action='store_true', default=not UNICODE_SUPPORT, help='Render layout box drawings in ASCII only.')
  169. @cli.argument('-r', '--rules-mk', action='store_true', help='Render the parsed values of the keyboard\'s rules.mk file.')
  170. @cli.argument('-a', '--api', action='store_true', help='Show fully processed info intended for API consumption.')
  171. @cli.subcommand('Keyboard information.')
  172. @automagic_keyboard
  173. @automagic_keymap
  174. def info(cli):
  175. """Compile an info.json for a particular keyboard and pretty-print it.
  176. """
  177. # Determine our keyboard(s)
  178. if not cli.config.info.keyboard:
  179. cli.log.error('Missing parameter: --keyboard')
  180. cli.subcommands['info'].print_help()
  181. return False
  182. if not is_keyboard(cli.config.info.keyboard):
  183. cli.log.error('Invalid keyboard: "%s"', cli.config.info.keyboard)
  184. return False
  185. if bool(cli.args.rules_mk):
  186. print_parsed_rules_mk(cli.config.info.keyboard)
  187. return False
  188. # default keymap stored in config file should be ignored
  189. if cli.config_source.info.keymap == 'config_file':
  190. cli.config_source.info.keymap = None
  191. # Build the info.json file
  192. if cli.config.info.keymap:
  193. kb_info_json = keymap_json(cli.config.info.keyboard, cli.config.info.keymap)
  194. else:
  195. kb_info_json = info_json(cli.config.info.keyboard)
  196. if not cli.args.api:
  197. kb_info_json = _strip_api_content(kb_info_json)
  198. # Output in the requested format
  199. if cli.args.format == 'json':
  200. print(json.dumps(kb_info_json, cls=InfoJSONEncoder, sort_keys=True))
  201. return True
  202. elif cli.args.format == 'text':
  203. print_dotted_output(kb_info_json)
  204. title_caps = False
  205. elif cli.args.format == 'friendly':
  206. print_friendly_output(kb_info_json)
  207. title_caps = True
  208. else:
  209. cli.log.error('Unknown format: %s', cli.args.format)
  210. return False
  211. # Output requested extras
  212. if cli.config.info.layouts:
  213. show_layouts(kb_info_json, title_caps)
  214. if cli.config.info.matrix:
  215. show_matrix(kb_info_json, title_caps)
  216. if cli.config.info.leds:
  217. show_leds(kb_info_json, title_caps)
  218. if cli.config.info.keymap:
  219. show_keymap(kb_info_json, title_caps)