3 # Copyright © 2020 Red Hat, Inc.
5 # Permission is hereby granted, free of charge, to any person obtaining a
6 # copy of this software and associated documentation files (the "Software"),
7 # to deal in the Software without restriction, including without limitation
8 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 # and/or sell copies of the Software, and to permit persons to whom the
10 # Software is furnished to do so, subject to the following conditions:
12 # The above copyright notice and this permission notice (including the next
13 # paragraph) shall be included in all copies or substantial portions of the
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 # DEALINGS IN THE SOFTWARE.
25 # This script creates the necessary scaffolding to create a custom keyboard
26 # layout or option. It does not actually configure anything, it merely creates
27 # the required directory structure and file scaffolding for the key
28 # configurations to be added by the user.
37 from pathlib import Path
38 from textwrap import dedent
41 # Default values are set by meson but to make this usable directly within the
42 # git source tree, use some sensible default values and return those where the
43 # meson define hasn't been replaced.
44 def default_value(key):
46 'extrapath': ('@XKBEXTRAPATH@', '/etc/xkb'),
47 'rules': ('@DEFAULT_XKB_RULES@', 'evdev'),
50 mesondefault, default = defaults[key]
51 if mesondefault.startswith('@') and mesondefault.endswith('@'):
57 logging.basicConfig(level=logging.DEBUG)
58 logger = logging.getLogger('xkbcli')
59 logger.setLevel(logging.INFO)
62 def create_directory_structure(basedir):
63 basedir.mkdir(exist_ok=True)
64 # Note: we skip geometry
65 for d in ['compat', 'keycodes', 'rules', 'symbols', 'types']:
66 (basedir / d).mkdir(exist_ok=True)
69 def create_rules_template(base_path, ruleset, layout, option):
70 rules = base_path / 'rules' / ruleset
72 logger.warning(f'Rules file {rules} already exists, skipping')
75 with open(rules, 'w') as rulesfile:
77 // generated by xkbcli scaffold-new-layout
79 // Note: no rules file entries are required for for a custom layout
82 rulesfile.write(header)
85 group, section = option
86 option_template = dedent(f'''\
87 // This section maps XKB option "{group}:{section}" to the '{section}' section in the
88 // 'symbols/{group}' file.
91 {group}:{section} = +{group}({section})
94 rulesfile.write(option_template)
97 // Include the system '{ruleset}' file
98 ! include %S/{ruleset}
100 rulesfile.write(footer)
103 def create_symbols_template(basedir, layout_variant, option):
104 if not layout_variant and not option:
105 logger.info('No layout or option given, skipping symbols templates')
108 layout, variant = layout_variant
109 layout_file = Path(basedir) / 'symbols' / layout
110 if layout_file.exists():
111 logger.warning(f'Symbols file {layout_file} already exists, skipping')
114 layout_fd = open(layout_file, 'w')
115 layout_fd.write('// generated by xkbcli scaffold-new-sources\n\n')
117 group, section = option
118 options_file = Path(basedir) / 'symbols' / group
120 # Cater for a potential "custom(variant)" layout and "custom:foo" option,
121 # i.e. where both layout and options use the same symbols file
122 if options_file == layout_file:
123 options_fd = layout_fd
124 elif options_file.exists():
125 logger.warning(f'File {options_file} already exists, skipping')
128 options_fd = open(options_file, 'w')
129 options_fd.write('// generated by xkbcli scaffold\n\n')
138 include = f'include "{layout}(basic)"'
140 logger.debug(f'Writing "{layout}({variant})" layout template to {layout_file}')
141 layout_fd.write(dedent(f'''\
142 {default}partial alphanumeric_keys modifier_keys
143 xkb_symbols "{variant}" {{
144 name[Group1]= "{variant} ({layout})";
149 // key <CAPS> {{ [ Escape ] }};
154 logger.debug(f'Writing "{section}" options template to {options_file}')
155 options_fd.write(dedent(f'''\
156 partial modifier_keys
157 xkb_symbols "{section}" {{
159 // key <CAPS> {{ [ Escape ] }};
167 def create_registry_template(basedir, ruleset, layout_variant, option):
168 xmlpath = Path(basedir) / 'rules' / f'{ruleset}.xml'
170 logger.warning(f'XML file {xmlpath} already exists, skipping')
173 with open(xmlpath, 'w') as xmlfile:
174 logger.debug(f'Writing XML file {xmlfile}')
177 layout, variant = layout_variant
179 variant_template = f'''
183 <name>{variant}</name>
184 <shortDescription>{variant}</shortDescription>
185 <description>{layout} ({variant})</description>
191 variant_template = ''
192 layout_template = f'''
196 <name>{layout}</name>
197 <shortDescription>{layout}</shortDescription>
198 <description>{layout}</description>
207 group, section = option
208 option_template = f'''
210 <group allowMultipleSelection="true">
213 <description>{group} options</description>
217 <name>{group}:{section}</name>
218 <description>{group}:{section} description</description>
227 template = dedent(f'''\
228 <?xml version="1.0" encoding="UTF-8"?>
229 <!DOCTYPE xkbConfigRegistry SYSTEM "xkb.dtd">
230 <!-- generated by xkbcli scaffold-new-layout -->
231 <xkbConfigRegistry version="1.1">
234 </xkbConfigRegistry>''')
235 xmlfile.write(template)
240 This tool creates the directory structure and template files for
241 a custom XKB layout and/or option.
243 Use the --option and --layout arguments to specify the template names to
244 use. These use default values, unset those with the empty string.
248 xkbcli scaffold --layout mylayout --option ''
249 xkbcli scaffold --layout '' --option 'custom:foo'
250 xkbcli scaffold --system --layout 'us(myvariant)' --option 'custom:foo'
252 This is a simple tool. If files already exist, the scaffolding skips
253 over that file and the result may not be correct.
256 parser = argparse.ArgumentParser(
257 description='Create scaffolding to configure custom keymaps',
258 formatter_class=argparse.RawDescriptionHelpFormatter,
260 parser.add_argument('-v', '--verbose', action='store_true', default=False,
261 help='Enable verbose debugging output')
262 group = parser.add_mutually_exclusive_group()
263 group.add_argument('--system', action='store_true', default=False,
264 help=f'Create scaffolding in {default_value("extrapath")}')
265 group.add_argument('--user', action='store_true', default=False,
266 help='Create scaffolding in $XDG_CONFIG_HOME/xkb')
267 parser.add_argument('--rules', type=str, default=default_value("rules"),
268 help=f'Ruleset name (default: "{default_value("rules")}")')
269 parser.add_argument('--layout', type=str, default='us(myvariant)',
270 help='Add scaffolding for a new layout or variant (default: "us(myvariant)")')
271 parser.add_argument('--option', type=str, default='custom:myoption',
272 help='Add scaffolding for a new option (default: "custom:myoption")')
274 args = parser.parse_args()
277 logger.setLevel(logging.DEBUG)
280 basedir = Path(default_value('extrapath'))
282 xdgdir = os.getenv('XDG_CONFIG_HOME')
284 home = os.getenv('HOME')
286 logger.error('Unable to resolve base directory from $XDG_CONFIG_HOME or $HOME')
288 xdgdir = Path(home) / '.config'
289 basedir = Path(xdgdir) / 'xkb'
293 group, section = args.option.split(':')
294 option = (group, section)
296 logger.error('Option must be specified as "group:name"')
302 # match either "us" or "us(intl)" style layouts
303 # [(] should be \( but flake8 complains about that
304 match = re.fullmatch('([a-z]+)([(][a-z]+[)])?', args.layout, flags=re.ASCII)
305 l, v = match.group(1, 2)
307 v = v.strip('()') # regex above includes ( )
308 layout_variant = l, v
310 layout_variant = None
313 create_directory_structure(basedir)
314 create_rules_template(basedir, args.rules, layout_variant, option)
315 create_symbols_template(basedir, layout_variant, option)
316 create_registry_template(basedir, args.rules, layout_variant, option)
317 except PermissionError as e:
321 print(f'XKB scaffolding for layout "{args.layout}" and option "{args.option}" is now in place.')
322 print(f'Edit the files in {basedir} to create the actual key mapping.')
325 if __name__ == '__main__':