Files @ 221117506281
Branch filter:

Location: majic-scripts/utils/mapping_generator.py

branko
[mapping_generator.py] Added support for YAML output to info command:

- The YAML output format for mappings should provide user with an easy
way to get started with assigning the mappings in template.
#!/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
# <https://www.gnu.org/licenses/>.


import shutil
import math

import click
import yaml

from defusedxml import ElementTree


def display_mappables_as_text(mappables):
    """
    Display mappables in text format.

    :param mappables: List of mappable IDs in the template.
    :type mappable: list[str]
    """

    # 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 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(mappables) <= row_threshold:
        rows = len(mappables)
    elif len(mappables) <= page_threshold:
        rows = row_threshold
        mappables.extend([''] * (page_threshold - len(mappables)))
    else:
        rows = math.ceil(len(mappables) / columns)
        mappables.extend([''] * (columns * rows - len(mappables)))

    click.echo('Mappables:\n')

    for i in range(rows):
        click.echo((" " * column_spacing).join([f'{m:<{column_width}}' for m in mappables[i::rows]]))


def display_mappables_as_yaml(mappables):
    """
    Display mappables in YAML format.

    :param mappables: List of mappable IDs in the template.
    :type mappable: list[str]
    """

    document = {
        'mappables': {}
    }

    for mappable in mappables:
        document['mappables'][mappable] = ''

    click.echo(yaml.dump(document, width=4096))


# 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')
@click.option('--output-format', '-f', type=click.Choice(['text', 'yaml']), default='text', help='output format')
def info(template, search, output_format):
    """
    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 output_format == 'yaml':
        click.echo('---\n')

    if not mappable_elements:
        error = 'No mappables found in the specified template. Please check that you have specified correct file.'
    elif not matched_mappables:
        error = 'Search term did not match any mappable in the specified template.'
    else:
        error = None

    if error and output_format == 'yaml':
        error = yaml.dump({'error': error}, width=4096)

    if error:
        click.echo(error)

    elif output_format == 'text':
        display_mappables_as_text(matched_mappables)

    elif output_format == 'yaml':
        display_mappables_as_yaml(matched_mappables)


if __name__ == '__main__':
    cli()