diff options
Diffstat (limited to 'tools/xkbcli-scaffold-new-layout.py')
-rwxr-xr-x | tools/xkbcli-scaffold-new-layout.py | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/tools/xkbcli-scaffold-new-layout.py b/tools/xkbcli-scaffold-new-layout.py new file mode 100755 index 0000000..a837b95 --- /dev/null +++ b/tools/xkbcli-scaffold-new-layout.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# +# Copyright © 2020 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + + +# This script creates the necessary scaffolding to create a custom keyboard +# layout or option. It does not actually configure anything, it merely creates +# the required directory structure and file scaffolding for the key +# configurations to be added by the user. +# + +import argparse +import logging +import os +import re +import sys + +from pathlib import Path +from textwrap import dedent + + +# Default values are set by meson but to make this usable directly within the +# git source tree, use some sensible default values and return those where the +# meson define hasn't been replaced. +def default_value(key): + defaults = { + 'extrapath': ('@XKBEXTRAPATH@', '/etc/xkb'), + 'rules': ('@DEFAULT_XKB_RULES@', 'evdev'), + } + + mesondefault, default = defaults[key] + if mesondefault.startswith('@') and mesondefault.endswith('@'): + return default + else: + return mesondefault + + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger('xkbcli') +logger.setLevel(logging.INFO) + + +def create_directory_structure(basedir): + basedir.mkdir(exist_ok=True) + # Note: we skip geometry + for d in ['compat', 'keycodes', 'rules', 'symbols', 'types']: + (basedir / d).mkdir(exist_ok=True) + + +def create_rules_template(base_path, ruleset, layout, option): + rules = base_path / 'rules' / ruleset + if rules.exists(): + logger.warning(f'Rules file {rules} already exists, skipping') + return + + with open(rules, 'w') as rulesfile: + header = dedent(f'''\ + // generated by xkbcli scaffold-new-layout + + // Note: no rules file entries are required for for a custom layout + + ''') + rulesfile.write(header) + + if option: + group, section = option + option_template = dedent(f'''\ + // This section maps XKB option "{group}:{section}" to the '{section}' section in the + // 'symbols/{group}' file. + // + ! option = symbols + {group}:{section} = +{group}({section}) + + ''') + rulesfile.write(option_template) + + footer = dedent(f'''\ + // Include the system '{ruleset}' file + ! include %S/{ruleset} + ''') + rulesfile.write(footer) + + +def create_symbols_template(basedir, layout_variant, option): + if not layout_variant and not option: + logger.info('No layout or option given, skipping symbols templates') + return + + layout, variant = layout_variant + layout_file = Path(basedir) / 'symbols' / layout + if layout_file.exists(): + logger.warning(f'Symbols file {layout_file} already exists, skipping') + layout_fd = None + else: + layout_fd = open(layout_file, 'w') + layout_fd.write('// generated by xkbcli scaffold-new-sources\n\n') + + group, section = option + options_file = Path(basedir) / 'symbols' / group + + # Cater for a potential "custom(variant)" layout and "custom:foo" option, + # i.e. where both layout and options use the same symbols file + if options_file == layout_file: + options_fd = layout_fd + elif options_file.exists(): + logger.warning(f'File {options_file} already exists, skipping') + options_fd = None + else: + options_fd = open(options_file, 'w') + options_fd.write('// generated by xkbcli scaffold\n\n') + + if layout_fd: + if variant is None: + default = 'default ' + variant = 'basic' + include = '' + else: + default = '' + include = f'include "{layout}(basic)"' + + logger.debug(f'Writing "{layout}({variant})" layout template to {layout_file}') + layout_fd.write(dedent(f'''\ + {default}partial alphanumeric_keys modifier_keys + xkb_symbols "{variant}" {{ + name[Group1]= "{variant} ({layout})"; + + {include} + + // Example: + // key <CAPS> {{ [ Escape ] }}; + }}; + ''')) + + if options_fd: + logger.debug(f'Writing "{section}" options template to {options_file}') + options_fd.write(dedent(f'''\ + partial modifier_keys + xkb_symbols "{section}" {{ + // Example: + // key <CAPS> {{ [ Escape ] }}; + }}; + ''')) + + layout_fd.close() + options_fd.close() + + +def create_registry_template(basedir, ruleset, layout_variant, option): + xmlpath = Path(basedir) / 'rules' / f'{ruleset}.xml' + if xmlpath.exists(): + logger.warning(f'XML file {xmlpath} already exists, skipping') + return + + with open(xmlpath, 'w') as xmlfile: + logger.debug(f'Writing XML file {xmlfile}') + + if layout_variant: + layout, variant = layout_variant + if variant: + variant_template = f''' + <variantList> + <variant> + <configItem> + <name>{variant}</name> + <shortDescription>{variant}</shortDescription> + <description>{layout} ({variant})</description> + </configItem> + </variant> + </variantList> + ''' + else: + variant_template = '' + layout_template = f''' + <layoutList> + <layout> + <configItem> + <name>{layout}</name> + <shortDescription>{layout}</shortDescription> + <description>{layout}</description> + </configItem> + {variant_template} + </layout> + </layoutList>''' + else: + layout_template = '' + + if option: + group, section = option + option_template = f''' + <optionList> + <group allowMultipleSelection="true"> + <configItem> + <name>{group}</name> + <description>{group} options</description> + </configItem> + <option> + <configItem> + <name>{group}:{section}</name> + <description>{group}:{section} description</description> + </configItem> + </option> + </group> + </optionList> + ''' + else: + option_template = '' + + template = dedent(f'''\ + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE xkbConfigRegistry SYSTEM "xkb.dtd"> + <!-- generated by xkbcli scaffold-new-layout --> + <xkbConfigRegistry version="1.1"> + {layout_template} + {option_template} + </xkbConfigRegistry>''') + xmlfile.write(template) + + +def main(): + epilog = dedent('''\ + This tool creates the directory structure and template files for + a custom XKB layout and/or option. + + Use the --option and --layout arguments to specify the template names to + use. These use default values, unset those with the empty string. + + Examples: + + xkbcli scaffold --layout mylayout --option '' + xkbcli scaffold --layout '' --option 'custom:foo' + xkbcli scaffold --system --layout 'us(myvariant)' --option 'custom:foo' + + This is a simple tool. If files already exist, the scaffolding skips + over that file and the result may not be correct. + ''') + + parser = argparse.ArgumentParser( + description='Create scaffolding to configure custom keymaps', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=epilog) + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='Enable verbose debugging output') + group = parser.add_mutually_exclusive_group() + group.add_argument('--system', action='store_true', default=False, + help=f'Create scaffolding in {default_value("extrapath")}') + group.add_argument('--user', action='store_true', default=False, + help='Create scaffolding in $XDG_CONFIG_HOME/xkb') + parser.add_argument('--rules', type=str, default=default_value("rules"), + help=f'Ruleset name (default: "{default_value("rules")}")') + parser.add_argument('--layout', type=str, default='us(myvariant)', + help='Add scaffolding for a new layout or variant (default: "us(myvariant)")') + parser.add_argument('--option', type=str, default='custom:myoption', + help='Add scaffolding for a new option (default: "custom:myoption")') + + args = parser.parse_args() + + if args.verbose: + logger.setLevel(logging.DEBUG) + + if args.system: + basedir = Path(default_value('extrapath')) + else: + xdgdir = os.getenv('XDG_CONFIG_HOME') + if not xdgdir: + home = os.getenv('HOME') + if not home: + logger.error('Unable to resolve base directory from $XDG_CONFIG_HOME or $HOME') + sys.exit(1) + xdgdir = Path(home) / '.config' + basedir = Path(xdgdir) / 'xkb' + + if args.option: + try: + group, section = args.option.split(':') + option = (group, section) + except ValueError: + logger.error('Option must be specified as "group:name"') + sys.exit(1) + else: + option = None + + if args.layout: + # match either "us" or "us(intl)" style layouts + # [(] should be \( but flake8 complains about that + match = re.fullmatch('([a-z]+)([(][a-z]+[)])?', args.layout, flags=re.ASCII) + l, v = match.group(1, 2) + if v: + v = v.strip('()') # regex above includes ( ) + layout_variant = l, v + else: + layout_variant = None + + try: + create_directory_structure(basedir) + create_rules_template(basedir, args.rules, layout_variant, option) + create_symbols_template(basedir, layout_variant, option) + create_registry_template(basedir, args.rules, layout_variant, option) + except PermissionError as e: + logger.critical(e) + sys.exit(1) + + print(f'XKB scaffolding for layout "{args.layout}" and option "{args.option}" is now in place.') + print(f'Edit the files in {basedir} to create the actual key mapping.') + + +if __name__ == '__main__': + main() |