at master 11 kB view raw
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)