diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000000000000000000000000000000000000..6385f98609d0a8864fd6a555d1f4ce8de714a00e --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,2 @@ +;; Set wrapping column for Emacs python-mode. +((python-mode . ((fill-column . 120)))) diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..79a16af7eeba9d878dc15877877fcfe822e8a098 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..7cb33e5679e12bd996d9df1bab769d2b49fb94b5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[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"]} diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000000000000000000000000000000000000..00a778ef7b8e25a9e2c0d7524b61aa4d024dbb4c --- /dev/null +++ b/requirements.in @@ -0,0 +1,9 @@ +# Safer XML parsing. +defusedxml ~= 0.7.0 + +# Command line option handling. +Click ~= 8.1.0 + +# Python script development. +flake8 +pip-tools diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f72cd28819bacdde7128911a447db3e6e2d60c62 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,38 @@ +# +# 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 diff --git a/utils/mapping_generator.py b/utils/mapping_generator.py new file mode 100755 index 0000000000000000000000000000000000000000..42af18c6ac9871181181ec93b7dafcd4b7d984a6 --- /dev/null +++ b/utils/mapping_generator.py @@ -0,0 +1,97 @@ +#!/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()