From d5fc2a7d026acdf0a13c43f2fe8d314f9b984d73 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 14 Feb 2024 15:39:10 +1000 Subject: rules: generate the option->{symbols,compat,types} mappings from our XML files The vast majority of options are a straightforward foo:bar = +foo(bar) mapping. Instead of maintaining those mappings by hand let's generate the rules file from the files we definitely maintain by hand: the base.xml (and base.extras.xml) files. This also makes it clearer which ones are exceptions and need to be filled in through other means (or ignored because the option does not affect any symbols). The resulting rules file is identical to the previous hand-generated one but it is alphabetically sorted and uses different whitespacing. For merge.py to work we need to also fix the whitespaces in rules/compat/0041-option_symbols.part, otherwise the duplicate header detection won't work. --- rules/0042-option_symbols.part | 187 ---------------------- rules/0043-option_compat.part | 8 - rules/0044-option_types.part | 10 -- rules/compat/0041-option_symbols.part | 12 +- rules/generate-options-symbols.py | 283 ++++++++++++++++++++++++++++++++++ rules/meson.build | 45 ++++-- 6 files changed, 323 insertions(+), 222 deletions(-) delete mode 100644 rules/0042-option_symbols.part delete mode 100644 rules/0043-option_compat.part delete mode 100644 rules/0044-option_types.part create mode 100755 rules/generate-options-symbols.py (limited to 'rules') diff --git a/rules/0042-option_symbols.part b/rules/0042-option_symbols.part deleted file mode 100644 index d190a632..00000000 --- a/rules/0042-option_symbols.part +++ /dev/null @@ -1,187 +0,0 @@ -! option = symbols - altwin:menu = +altwin(menu) - altwin:menu_win = +altwin(menu_win) - altwin:meta_alt = +altwin(meta_alt) - altwin:alt_win = +altwin(alt_win) - altwin:ctrl_win = +altwin(ctrl_win) - altwin:ctrl_rwin = +altwin(ctrl_rwin) - altwin:ctrl_alt_win = +altwin(ctrl_alt_win) - altwin:meta_win = +altwin(meta_win) - altwin:left_meta_win = +altwin(left_meta_win) - altwin:hyper_win = +altwin(hyper_win) - altwin:alt_super_win = +altwin(alt_super_win) - altwin:swap_lalt_lwin = +altwin(swap_lalt_lwin) - altwin:swap_alt_win = +altwin(swap_alt_win) - altwin:prtsc_rwin = +altwin(prtsc_rwin) - caps:capslock = +capslock(capslock) - caps:numlock = +capslock(numlock) - caps:shiftlock = +capslock(shiftlock) - caps:swapescape = +capslock(swapescape) - caps:escape = +capslock(escape) - caps:escape_shifted_capslock = +capslock(escape_shifted_capslock) - caps:escape_shifted_compose = +capslock(escape_shifted_compose) - caps:backspace = +capslock(backspace) - caps:super = +capslock(super) - caps:hyper = +capslock(hyper) - caps:menu = +capslock(menu) - caps:none = +capslock(none) - caps:ctrl_modifier = +capslock(ctrl_modifier) - ctrl:nocaps = +ctrl(nocaps) - ctrl:lctrl_meta = +ctrl(lctrl_meta) - ctrl:swapcaps = +ctrl(swapcaps) - ctrl:hyper_capscontrol = +ctrl(hyper_capscontrol) - ctrl:grouptoggle_capscontrol = +ctrl(grouptoggle_capscontrol) - ctrl:ac_ctrl = +ctrl(ac_ctrl) - ctrl:aa_ctrl = +ctrl(aa_ctrl) - ctrl:rctrl_ralt = +ctrl(rctrl_ralt) - ctrl:menu_rctrl = +ctrl(menu_rctrl) - ctrl:ralt_rctrl = +ctrl(ralt_rctrl) - ctrl:swap_lalt_lctl = +ctrl(swap_lalt_lctl) - ctrl:swap_ralt_rctl = +ctrl(swap_ralt_rctl) - ctrl:swap_lwin_lctl = +ctrl(swap_lwin_lctl) - ctrl:swap_rwin_rctl = +ctrl(swap_rwin_rctl) - ctrl:swap_lalt_lctl_lwin = +ctrl(swap_lalt_lctl_lwin) - compose:ralt = +compose(ralt) - compose:lwin = +compose(lwin) - compose:lwin-altgr = +compose(lwin-altgr) - compose:rwin = +compose(rwin) - compose:rwin-altgr = +compose(rwin-altgr) - compose:menu = +compose(menu) - compose:menu-altgr = +compose(menu-altgr) - compose:lctrl = +compose(lctrl) - compose:lctrl-altgr = +compose(lctrl-altgr) - compose:rctrl = +compose(rctrl) - compose:rctrl-altgr = +compose(rctrl-altgr) - compose:caps = +compose(caps) - compose:caps-altgr = +compose(caps-altgr) - compose:102 = +compose(102) - compose:102-altgr = +compose(102-altgr) - compose:ins = +compose(ins) - compose:paus = +compose(paus) - compose:prsc = +compose(prsc) - compose:sclk = +compose(sclk) - keypad:oss = +keypad(oss) - keypad:legacy = +keypad(legacy) - keypad:legacy_wang = +keypad(legacy_wang) - keypad:oss_wang = +keypad(oss_wang) - keypad:future = +keypad(future) - keypad:future_wang = +keypad(future_wang) - keypad:hex = +keypad(hex) - keypad:atm = +keypad(atm) - keypad:pointerkeys = +keypad(pointerkeys) - kpdl:dot = +kpdl(dot) - kpdl:comma = +kpdl(comma) - kpdl:dotoss = +kpdl(dotoss) - kpdl:dotoss_latin9 = +kpdl(dotoss_latin9) - kpdl:commaoss = +kpdl(commaoss) - kpdl:momayyezoss = +kpdl(momayyezoss) - kpdl:kposs = +kpdl(kposs) - kpdl:semi = +kpdl(semi) - nbsp:none = +nbsp(none) - nbsp:level2 = +nbsp(level2) - nbsp:level3 = +nbsp(level3) - nbsp:level3n = +nbsp(level3n) - nbsp:level4 = +nbsp(level4) - nbsp:level4n = +nbsp(level4n) - nbsp:level4nl = +nbsp(level4nl) - nbsp:zwnj2 = +nbsp(zwnj2) - nbsp:zwnj2zwj3 = +nbsp(zwnj2zwj3) - nbsp:zwnj2zwj3nb4 = +nbsp(zwnj2zwj3nb4) - nbsp:zwnj2nb3 = +nbsp(zwnj2nb3) - nbsp:zwnj2nb3zwj4 = +nbsp(zwnj2nb3zwj4) - nbsp:zwnj2nb3nnb4 = +nbsp(zwnj2nb3nnb4) - nbsp:zwnj3zwj4 = +nbsp(zwnj3zwj4) - eurosign:e = +eurosign(e) - eurosign:E = +eurosign(E) - eurosign:2 = +eurosign(2) - eurosign:4 = +eurosign(4) - eurosign:5 = +eurosign(5) - rupeesign:4 = +rupeesign(4) - esperanto:qwerty = +epo(qwerty) - esperanto:dvorak = +epo(dvorak) - esperanto:colemak = +epo(colemak) - parens:swap_brackets = +parens(swap_brackets) - japan:nicola_f_bs = +jp(nicola_f_bs) - japan:hztg_escape = +jp(hztg_escape) - korean:ralt_hangul = +kr(ralt_hangul) - korean:rctrl_hangul = +kr(rctrl_hangul) - korean:ralt_hanja = +kr(ralt_hanja) - korean:rctrl_hanja = +kr(rctrl_hanja) - grab:debug = +grab(debug) - srvrkeys:none = +srvrkeys(none) - terminate:ctrl_alt_bksp = +terminate(ctrl_alt_bksp) - apple:alupckeys = +macintosh_vndr/apple(alupckeys) - apple:jp_pc106 = +macintosh_vndr/apple(jp_pc106) - apple:jp_oadg109a = +macintosh_vndr/apple(jp_oadg109a) - solaris:sun_compat = +sun_vndr/solaris(sun_compat) - shift:breaks_caps = +shift(breaks_caps) - shift:both_capslock = +shift(both_capslock) - shift:both_capslock_cancel = +shift(both_capslock_cancel) - shift:both_shiftlock = +shift(both_shiftlock) - lv2:lsgt_switch = +level2(lsgt_switch) -// Third-level choosers: - lv3:switch = +level3(switch) - lv3:ralt_switch = +level3(ralt_switch) - lv3:ralt_switch_multikey = +level3(ralt_switch_multikey) - lv3:lalt_switch = +level3(lalt_switch) - lv3:alt_switch = +level3(alt_switch) - lv3:menu_switch = +level3(menu_switch) - lv3:win_switch = +level3(win_switch) - lv3:lwin_switch = +level3(lwin_switch) - lv3:rwin_switch = +level3(rwin_switch) - lv3:enter_switch = +level3(enter_switch) - lv3:caps_switch = +level3(caps_switch) - lv3:bksl_switch = +level3(bksl_switch) - lv3:lsgt_switch = +level3(lsgt_switch) - lv3:caps_switch_latch = +level3(caps_switch_latch) - lv3:bksl_switch_latch = +level3(bksl_switch_latch) - lv3:lsgt_switch_latch = +level3(lsgt_switch_latch) - lv3:4_switch_isolated = +level3(4_switch_isolated) - lv3:9_switch_isolated = +level3(9_switch_isolated) -// Fifth-level choosers: - lv5:caps_switch = +level5(caps_switch) - lv5:lsgt_switch = +level5(lsgt_switch) - lv5:ralt_switch = +level5(ralt_switch) - lv5:menu_switch = +level5(menu_switch) - lv5:rctrl_switch = +level5(rctrl_switch) - lv5:lsgt_switch_lock = +level5(lsgt_switch_lock) - lv5:ralt_switch_lock = +level5(ralt_switch_lock) - lv5:lwin_switch_lock = +level5(lwin_switch_lock) - lv5:rwin_switch_lock = +level5(rwin_switch_lock) - grp:switch = +group(switch) - grp:lswitch = +group(lswitch) - grp:win_switch = +group(win_switch) - grp:lwin_switch = +group(lwin_switch) - grp:rwin_switch = +group(rwin_switch) - grp:rctrl_switch = +group(rctrl_switch) - grp:menu_switch = +group(menu_switch) - grp:caps_switch = +group(caps_switch) - grp:ctrls_toggle = +group(ctrls_toggle) - grp:caps_toggle = +group(caps_toggle) - grp:shift_caps_toggle = +group(shift_caps_toggle) - grp:caps_select = +group(caps_select) - grp:win_menu_select = +group(win_menu_select) - grp:ctrl_select = +group(ctrl_select) - grp:alt_caps_toggle = +group(alt_caps_toggle) - grp:menu_toggle = +group(menu_toggle) - grp:lwin_toggle = +group(lwin_toggle) - grp:rwin_toggle = +group(rwin_toggle) - grp:lshift_toggle = +group(lshift_toggle) - grp:rshift_toggle = +group(rshift_toggle) - grp:lctrl_toggle = +group(lctrl_toggle) - grp:rctrl_toggle = +group(rctrl_toggle) - grp:lalt_toggle = +group(lalt_toggle) - grp:sclk_toggle = +group(sclk_toggle) - grp:lctrl_lwin_toggle = +group(lctrl_lwin_toggle) - grp:lctrl_lwin_rctrl_menu = +group(lctrl_lwin_rctrl_menu) - grp:lctrl_lalt_toggle = +group(lctrl_lalt_toggle) - grp:rctrl_ralt_toggle = +group(rctrl_ralt_toggle) - grp:ctrl_alt_toggle = +group(ctrl_alt_toggle) - grp:ctrl_alt_toggle_bidir = +group(ctrl_alt_toggle_bidir) - grp:lctrl_lshift_toggle = +group(lctrl_lshift_toggle) - grp:ctrl_shift_toggle = +group(ctrl_shift_toggle) - grp:ctrl_shift_toggle_bidir = +group(ctrl_shift_toggle_bidir) - grp:lalt_lshift_toggle = +group(lalt_lshift_toggle) - grp:ralt_rshift_toggle = +group(ralt_rshift_toggle) - grp:alt_shift_toggle = +group(alt_shift_toggle) - grp:alt_shift_toggle_bidir = +group(alt_shift_toggle_bidir) diff --git a/rules/0043-option_compat.part b/rules/0043-option_compat.part deleted file mode 100644 index 87cf7658..00000000 --- a/rules/0043-option_compat.part +++ /dev/null @@ -1,8 +0,0 @@ -! option = compat - grp_led:num = +grp_led(num) - grp_led:caps = +grp_led(caps) - grp_led:scroll = +grp_led(scroll) - mod_led:compose = +mod_led(compose) - japan:kana_lock = +japan(kana_lock) - caps:shiftlock = +caps(shiftlock) - grab:break_actions = +grab(break_actions) diff --git a/rules/0044-option_types.part b/rules/0044-option_types.part deleted file mode 100644 index ce7ebe55..00000000 --- a/rules/0044-option_types.part +++ /dev/null @@ -1,10 +0,0 @@ -! option = types - caps:internal = +caps(internal) - caps:internal_nocancel = +caps(internal_nocancel) - caps:shift = +caps(shift) - caps:shift_nocancel = +caps(shift_nocancel) - numpad:pc = +numpad(pc) - numpad:mac = +numpad(mac) - numpad:microsoft = +numpad(microsoft) - numpad:shift3 = +numpad(shift3) - custom:types = +custom diff --git a/rules/compat/0041-option_symbols.part b/rules/compat/0041-option_symbols.part index 89bc6859..088a5236 100644 --- a/rules/compat/0041-option_symbols.part +++ b/rules/compat/0041-option_symbols.part @@ -1,8 +1,8 @@ -! option = symbols - grp:shift_caps_switch = +group(caps_select) - grp:win_menu_switch = +group(win_menu_select) - grp:lctrl_rctrl_switch = +group(ctrl_select) +! option = symbols + grp:shift_caps_switch = +group(caps_select) + grp:win_menu_switch = +group(win_menu_select) + grp:lctrl_rctrl_switch = +group(ctrl_select) // Delete the above three aliases in July 2027. - ctrl:swapcaps_hyper = +ctrl(hyper_capscontrol) - ctrl:swapcaps_and_switch_layout = +ctrl(swapcaps)+group(lctrl_toggle) + ctrl:swapcaps_hyper = +ctrl(hyper_capscontrol) + ctrl:swapcaps_and_switch_layout = +ctrl(swapcaps)+group(lctrl_toggle) // Delete the above two aliases in September 2027. diff --git a/rules/generate-options-symbols.py b/rules/generate-options-symbols.py new file mode 100755 index 00000000..fcd3906a --- /dev/null +++ b/rules/generate-options-symbols.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# +# This file is formatted with python black +# +# This file parses the base.xml and base.extras.xml file and prints out the option->symbols +# mapping compatible with the rules format. See the meson.build file for how this is used. + +import argparse +import sys +import xml.etree.ElementTree as ET + +from typing import Iterable +from dataclasses import dataclass +from pathlib import Path + +XKB_CONFIG_ROOT: "XkbConfigRoot" + +skip = ( + # Special type that exists but doesn't really exist + "custom:types", + # These are all defined in 0036-layoutoption_symbols.part + "misc:apl", + "misc:typo", + "lv3:ralt_alt", + "grp:toggle", + "grp:alts_toggle", + "grp:alt_altgr_toggle", + "grp:alt_space_toggle", + "grp:win_space_toggle", + "grp:ctrl_space_toggle", + "grp:rctrl_rshift_toggle", + "grp:shifts_toggle", +) + + +def error(msg): + print(f"ERROR: {msg}", file=sys.stderr) + print("Aborting now") + sys.exit(1) + + +@dataclass +class Directive: + option: "Option" + filename: str + section: str + + @property + def name(self) -> str: + return self.option.name + + def __str__(self) -> str: + return f"{self.filename}({self.section})" + + +@dataclass +class DirectiveSet: + option: "Option" + keycodes: Directive | None + compat: Directive | None + geometry: Directive | None + symbols: Directive | None + types: Directive | None + + @property + def is_empty(self) -> bool: + return all( + x is None + for x in ( + self.keycodes, + self.compat, + self.geometry, + self.symbols, + self.types, + ) + ) + + def for_section(self, section: str) -> Directive | None: + return { + "xkb_keycodes": self.keycodes, + "xkb_compatibility": self.compat, + "xkb_geometry": self.geometry, + "xkb_symbols": self.symbols, + "xkb_types": self.types, + }[section] + + +@dataclass +class XkbConfigRoot: + keycodes: Path + compat: Path + geometry: Path + symbols: Path + types: Path + + @property + def directories(self) -> Iterable[Path]: + yield self.keycodes + yield self.compat + yield self.geometry + yield self.symbols + yield self.types + + @property + def section_headers(self) -> Iterable[str]: + for h in [ + "xkb_keycodes", + "xkb_compatibility", + "xkb_geometry", + "xkb_symbols", + "xkb_types", + ]: + yield h + + @classmethod + def for_basedir(cls, basedir: Path) -> "XkbConfigRoot": + return cls( + keycodes=basedir / "keycodes", + compat=basedir / "compat", + geometry=basedir / "geometry", + symbols=basedir / "symbols", + types=basedir / "types", + ) + + +@dataclass +class Option: + """ + Wrapper around a single option -> symbols rules file entry. Has the properties + name and directive where the directive consists of the XKB symbols file name + and corresponding section, usually composed in the rules file as: + name = +directive + """ + + name: str + + def __lt__(self, other) -> bool: + return self.name < other.name + + @property + def directive(self) -> Directive: + f, s = self.name.split(":") + return Directive(self, f, s) + + +def resolve_option(option: Option) -> DirectiveSet: + directives: dict[str, Directive | None] = { + s: None for s in XKB_CONFIG_ROOT.section_headers + } + directive = option.directive + filename, section = directive.filename, directive.section + for subdir, section_header in zip( + XKB_CONFIG_ROOT.directories, XKB_CONFIG_ROOT.section_headers + ): + if not (subdir / filename).exists(): + # Some of our foo:bar entries map to a baz_vndr/foo file + for vndr in subdir.glob("*_vndr"): + vndr_path = vndr / filename + if vndr_path.exists(): + filename = vndr_path.relative_to(subdir).as_posix() + break + else: + continue + + # Now check if the target file actually has that section + f = subdir / filename + with open(f) as fd: + found = any(f'{section_header} "{section}"' in line for line in fd) + if found: + directives[section_header] = Directive(option, filename, section) + + return DirectiveSet( + option=option, + keycodes=directives["xkb_keycodes"], + compat=directives["xkb_compatibility"], + geometry=directives["xkb_geometry"], + symbols=directives["xkb_symbols"], + types=directives["xkb_types"], + ) + + +def options(rules_xml) -> Iterable[Option]: + """ + Yields all Options from the given XML file + """ + tree = ET.parse(rules_xml) + root = tree.getroot() + + def fetch_subelement(parent, name): + sub_element = parent.findall(name) + if sub_element is not None and len(sub_element) == 1: + return sub_element[0] + return None + + def fetch_text(parent, name): + sub_element = fetch_subelement(parent, name) + if sub_element is None: + return None + return sub_element.text + + def fetch_name(elem): + try: + ci_element = ( + elem + if elem.tag == "configItem" + else fetch_subelement(elem, "configItem") + ) + name = fetch_text(ci_element, "name") + assert name is not None + return name + except AssertionError as e: + endl = "\n" # f{} cannot contain backslashes + e.args = ( + f"\nFor element {ET.tostring(elem).decode('utf-8')}\n{endl.join(e.args)}", + ) + raise + + for option in root.iter("option"): + yield Option(fetch_name(option)) + + +def main(): + global XKB_CONFIG_ROOT + + parser = argparse.ArgumentParser(description="Generate the evdev keycode lists.") + parser.add_argument( + "--xkb-config-root", + help="The XKB base directory", + default=Path("."), + type=Path, + ) + parser.add_argument( + "--rules-section", + choices=["xkb_symbols", "xkb_compatibility", "xkb_types"], + help="The rules section to generate", + default="xkb_symbols", + ) + parser.add_argument( + "files", nargs="+", help="The base.xml and base.extras.xml files" + ) + ns = parser.parse_args() + + XKB_CONFIG_ROOT = XkbConfigRoot.for_basedir(ns.xkb_config_root) + + all_options = [] + for f in ns.files: + os = list(options(f)) + all_options.extend(os) + + directives = (resolve_option(o) for o in sorted(all_options) if o.name not in skip) + + def check_and_map(directive): + assert ( + not directive.is_empty + ), f"Option {directive.option} does not resolve to any section" + + return directive.for_section(ns.rules_section) + + filtered = ( + x + for x in filter( + lambda y: y is not None, + map(check_and_map, directives), + ) + ) + + header = { + "xkb_symbols": "symbols", + "xkb_compatibility": "compat", + "xkb_types": "types", + }[ns.rules_section] + + print(f"! option = {header}") + for d in filtered: + assert d is not None + print(f" {d.name:30s} = +{d}") + + if ns.rules_section == "xkb_types": + print(f" {'custom:types':30s} = +custom") + + +if __name__ == "__main__": + main() diff --git a/rules/meson.build b/rules/meson.build index 514d03d9..509206dc 100644 --- a/rules/meson.build +++ b/rules/meson.build @@ -2,6 +2,9 @@ install_data('README', 'xkb.dtd', 'xfree98', install_dir: dir_xkb_rules) +base_xml = files('base.xml') +base_extras_xml = files('base.extras.xml') + # the actual rules files are generated from a list of parts in a very # specific order parts = [ @@ -30,13 +33,33 @@ parts = [ '0038-layout2option_symbols.part', '0039-layout3option_symbols.part', '0040-layout4option_symbols.part', - '0042-option_symbols.part', - '0043-option_compat.part', - '0044-option_types.part', + # 0042-option_symbols.part is generated from base{.extras}.xml + # 0043-option_compat.part is generated from base{.extras}.xml + # 0044-option_types.part is generated from base{.extras}.xml ] -# generated compat parts -rules_compat_generated = [] +rules_parts_generated = [] + +generate_rules_options_symbols = find_program('generate-options-symbols.py') +generated = [ + ['0042-option_symbols.part', 'xkb_symbols'], + ['0043-option_compat.part', 'xkb_compatibility'], + ['0044-option_types.part', 'xkb_types'], +] +foreach g: generated + filename = g[0] + section = g[1] + part = custom_target(filename, + build_by_default: true, + command: [generate_rules_options_symbols, + '--rules-section=@0@'.format(section), + '--xkb-config-root', meson.project_source_root(), + base_xml, base_extras_xml], + output: filename, + capture: true, + install: false) + rules_parts_generated += [part] +endforeach if get_option('compat-rules') # non-generated compat parts @@ -85,7 +108,7 @@ if get_option('compat-rules') ], output: ml_s_file, install: false) - rules_compat_generated += [ml_s] + rules_parts_generated += [ml_s] mlv_s_file = lvl_mlv_s['@0@'.format(lvl)] mlv_s = custom_target(mlv_s_file, @@ -99,7 +122,7 @@ if get_option('compat-rules') ], output: mlv_s_file, install: false) - rules_compat_generated += [mlv_s] + rules_parts_generated += [mlv_s] endforeach endif # compat-rules @@ -127,9 +150,9 @@ foreach ruleset: ['base', 'evdev'] build_by_default: true, command: [ merge_py, - rules_parts + rules_compat_generated, + rules_parts + rules_parts_generated, ], - depends: rules_compat_generated, + depends: rules_parts_generated, output: ruleset, capture: true, install: true, @@ -137,7 +160,7 @@ foreach ruleset: ['base', 'evdev'] # Third: the xml files, simply copied from the base*.xml files ruleset_xml = configure_file(output: '@0@.xml'.format(ruleset), - input: 'base.xml', + input: base_xml, copy: true, install: true, install_dir: dir_xkb_rules) @@ -148,7 +171,7 @@ foreach ruleset: ['base', 'evdev'] endif configure_file(output: '@0@.extras.xml'.format(ruleset), - input: 'base.extras.xml', + input: base_extras_xml, copy: true, install: true, install_dir: dir_xkb_rules) -- cgit v1.2.3