Changeset - f3d3009808f7
[Not reviewed]
0 0 6
Branko Majic (branko) - 1 month ago 2025-03-02 00:54:21
branko@majic.rs
[mapping_generator.py] Initial implementation with support for showing tempalte information:

- Set-up Python project structure to make it easier to install and use
the Python scripts from within confines of a virtual environment.
- Set-up custom configuration for line breaks for Python file editing
when using GNU Emacs.
6 files changed with 189 insertions and 0 deletions:
0 comments (0 inline, 0 general) First comment
.dir-locals.el
Show inline comments
 
new file 100644
 
;; Set wrapping column for Emacs python-mode.
 
((python-mode . ((fill-column . 120))))
.flake8
Show inline comments
 
new file 100644
 
[flake8]
 
max-line-length = 120
 
\ No newline at end of file
pyproject.toml
Show inline comments
 
new file 100644
 
[project]
 
name = "majic-scripts"
 
description = "Collection of various scripts, primarly oriented towards personal use."
 
license = {text = "GPL-3.0-or-later"}
 
authors = [{name = "Branko Majic", email = "branko@majic.rs"}]
 
maintainers = [{name = "Branko Majic", email = "branko@majic.rs"}]
 

	
 
dynamic = ["dependencies"]
 

	
 
version = "2025.03.02"
 
requires-python = ">= 3.11"
 

	
 
classifiers = [
 
  "Development Status  :: 4 - Beta",
 
  "Environment :: Console",
 
  "Intended Audience :: Developers",
 
  "Intended Audience :: End Users/Desktop",
 
  "Intended Audience :: System Administrators",
 
  "Natural Language :: English",
 
  "Operating System :: POSIX :: Linux",
 
  "Programming Language :: Python :: 3.11",
 
  "Programming Language :: Unix Shell",
 
  "Topic :: Utilities",
 
]
 

	
 
[project.urls]
 
Homepage = "https://code.majic.rs/majic-scripts"
 
Repository = "https://code.majic.rs/majic-scripts"
 

	
 
[project.scripts]
 
mapping-generator = "utils.mapping_generator:cli"
 

	
 
[build-system]
 
requires = ["setuptools"]
 
build-backend = "setuptools.build_meta"
 

	
 
[tool.setuptools]
 
packages = ["utils"]
 

	
 
[tool.setuptools.dynamic]
 
dependencies = {file  = ["requirements.txt"]}
requirements.in
Show inline comments
 
new file 100644
 
# Safer XML parsing.
 
defusedxml ~= 0.7.0
 

	
 
# Command line option handling.
 
Click ~= 8.1.0
 

	
 
# Python script development.
 
flake8
 
pip-tools
requirements.txt
Show inline comments
 
new file 100644
 
#
 
# This file is autogenerated by pip-compile with Python 3.11
 
# by the following command:
 
#
 
#    pip-compile --allow-unsafe
 
#
 
build==1.2.2.post1
 
    # via pip-tools
 
click==8.1.8
 
    # via
 
    #   -r requirements.in
 
    #   pip-tools
 
defusedxml==0.7.1
 
    # via -r requirements.in
 
flake8==7.1.2
 
    # via -r requirements.in
 
mccabe==0.7.0
 
    # via flake8
 
packaging==24.2
 
    # via build
 
pip-tools==7.4.1
 
    # via -r requirements.in
 
pycodestyle==2.12.1
 
    # via flake8
 
pyflakes==3.2.0
 
    # via flake8
 
pyproject-hooks==1.2.0
 
    # via
 
    #   build
 
    #   pip-tools
 
wheel==0.45.1
 
    # via pip-tools
 

	
 
# The following packages are considered to be unsafe in a requirements file:
 
pip==25.0.1
 
    # via pip-tools
 
setuptools==75.8.2
 
    # via pip-tools
utils/mapping_generator.py
Show inline comments
 
new file 100755
 
#!/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
 

	
 
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()
0 comments (0 inline, 0 general) First comment
You need to be logged in to comment. Login now