#!/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 {{ [ 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 {{ [ 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''' {variant} {variant} {layout} ({variant}) ''' else: variant_template = '' layout_template = f''' {layout} {layout} {layout} {variant_template} ''' else: layout_template = '' if option: group, section = option option_template = f''' {group} {group} options ''' else: option_template = '' template = dedent(f'''\ {layout_template} {option_template} ''') 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()