summaryrefslogtreecommitdiff
path: root/tools/xkbcli-scaffold-new-layout.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/xkbcli-scaffold-new-layout.py')
-rwxr-xr-xtools/xkbcli-scaffold-new-layout.py326
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()