qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

info.py (11011B)


      1 """Keyboard information script.
      2 
      3 Compile an info.json for a particular keyboard and pretty-print it.
      4 """
      5 import sys
      6 import json
      7 
      8 from milc import cli
      9 
     10 from qmk.json_encoders import InfoJSONEncoder
     11 from qmk.constants import COL_LETTERS, ROW_LETTERS
     12 from qmk.decorators import automagic_keyboard, automagic_keymap
     13 from qmk.keyboard import keyboard_completer, keyboard_folder, render_layouts, render_layout, rules_mk
     14 from qmk.info import info_json, keymap_json
     15 from qmk.keymap import locate_keymap
     16 from qmk.path import is_keyboard
     17 
     18 UNICODE_SUPPORT = sys.stdout.encoding.lower().startswith('utf')
     19 
     20 
     21 def _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 
     48 def 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 
     72 def 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 
     81 def 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 
    105 def 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 
    147 def 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 
    166 def 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 
    185 def 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 
    203 def 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
    222 def 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)