#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2025 Branko Majic # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with this program. If not, see # . import shutil import math import click from defusedxml import ElementTree # Allow use of short option for showing help. CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.group(context_settings=CONTEXT_SETTINGS) def cli(): """ Generate input mapping cheatsheets for input devices using specially-crafted scalable vector graphics (SVG) templates. To prepare a template, create an SVG using Inkscape, and assign custom IDs to text elements that should be managed by the mapping generator. """ pass @cli.command() @click.argument('template', type=click.File('r')) @click.option('--search', '-s', default='', help='only show matching mappings') def info(template, search): """ Shows information about the passed-in SVG template. This currently includes: - List of mappable elements. """ template_tree = ElementTree.fromstring(template.read()) namespaces = { 'svg': 'http://www.w3.org/2000/svg', } # Inkscape default ID starts with string 'text' unless user assigns custom ID. mappable_elements = [ e for e in template_tree.findall('.//svg:text', namespaces) if not e.get('id').startswith('text') ] matched_mappables = sorted([e.get('id') for e in mappable_elements if search in e.get('id')]) if not mappable_elements: click.echo('No mappables found in the specified template. Please check that you have specified correct file.') elif not matched_mappables: click.echo('Search term did not match any mappable in the specified template.') else: # Show mappables in multiple columns, while enforcing a minimum column height (element count). For example, if # there are only 5 mappables, show them in a single column instead of splitting them up into multiple ones. terminal_size = shutil.get_terminal_size((80, 20)) column_spacing = 4 row_threshold = math.floor(terminal_size.lines * 0.5) column_width = max([len(m) for m in matched_mappables]) columns = math.floor((terminal_size.columns) / (column_width + column_spacing)) page_threshold = row_threshold * columns # Expand the list with blank strings in order to have exact number of 'cells' when printing to # screeen. Simplifies the code dealing with printing a bit and avoid invalid index access. if len(matched_mappables) <= row_threshold: rows = len(matched_mappables) elif len(matched_mappables) <= page_threshold: rows = row_threshold matched_mappables.extend([''] * (page_threshold - len(matched_mappables))) else: rows = math.ceil(len(matched_mappables) / columns) matched_mappables.extend([''] * (columns * rows - len(matched_mappables))) click.echo('Mappables:\n') for i in range(rows): click.echo((" " * column_spacing).join([f'{m:<{column_width}}' for m in matched_mappables[i::rows]])) if __name__ == '__main__': cli()