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