#!/usr/bin/env python3 # Copyright © 2025 Pierre Le Marre # SPDX-License-Identifier: MIT """ This script generate tests for merge modes. """ from __future__ import annotations import argparse import dataclasses import itertools import re import textwrap from abc import ABCMeta, abstractmethod from collections import defaultdict from collections.abc import Callable, Generator, Iterable, Iterator, Sequence from dataclasses import dataclass from enum import Flag, IntFlag, StrEnum, auto, unique from pathlib import Path from string import Template from typing import Any, ClassVar, NewType, Self import jinja2 SCRIPT = Path(__file__) # Root of the project ROOT = SCRIPT.parent.parent ################################################################################ # # XKB compiler # ################################################################################ @dataclass class XkbCompiler: name: str extended_syntax: bool default: ClassVar[str] = "xkbcommon" @property def is_default(self) -> bool: return self.name == self.default @property def suffix(self) -> str: return "" if self.is_default else "-" + self.name @classmethod def parse(cls, name: str) -> Self: match name: case cls.default: return cls(name=name, extended_syntax=True) case "xkbcomp": return cls(name=name, extended_syntax=False) case "kbvm": return cls(name=name, extended_syntax=True) case _: raise ValueError(f"Invalid XKB compiler: {name}") ################################################################################ # # Merge modes # ################################################################################ @unique class MergeMode(StrEnum): Default = auto() Augment = auto() Override = auto() Replace = auto() @property def include(self) -> str: if self is self.__class__.Default: return "include" else: return self @property def char(self) -> str: match self: case self.__class__.Default: return "+" case self.__class__.Augment: return "|" case self.__class__.Override: return "+" case self.__class__.Replace: return "^" case _: raise ValueError(self) INDENT = "\t" ################################################################################ # # Components # ################################################################################ @unique class Component(StrEnum): keycodes = auto() types = auto() compat = auto() symbols = auto() def render_keymap(self, content) -> Generator[str]: yield "xkb_keymap {" for component in self.__class__: yield from ( textwrap.indent(t, INDENT) for t in self.render(component, content if component is self else "") ) yield "};" @classmethod def render(cls, component: Self, content: str, name: str = "") -> Generator[str]: if not content: match component: case cls.keycodes: pass case cls.types: pass case cls.compat: content = "interpret.repeat= True;" case cls.symbols: pass if name: name = f' "{name}"' if content: yield f'xkb_{component}{name} "" {{' yield textwrap.indent(content, INDENT) yield "};" else: yield f'xkb_{component}{name} "" {{}};' class ComponentTemplate(Template): def __init__(self, template) -> Self: super().__init__(textwrap.dedent(template).strip()) def __bool__(self) -> bool: return bool(self.template) def render(self, merge: MergeMode) -> str: mode = "" if merge is MergeMode.Default else f"{merge} " return self.substitute(mode=mode) def __add__(self, other: Any) -> Self: if isinstance(other, str): return self.__class__(self.template + "\n" + other) elif isinstance(other, Template): return self.__class__(self.template + "\n" + other.template) else: return NotImplemented def __radd__(self, other: Any) -> Self: if isinstance(other, str): return self.__class__(other + "\n" + self.template) elif isinstance(other, Template): return self.__class__(other.template + "\n" + self.template) else: return NotImplemented ################################################################################ # # Test templates # ################################################################################ @dataclass class Test: title: str file: Path content: str expected: str @dataclass class TestGroup: title: str tests: Sequence[Test] c_template: ClassVar[Path] = Path("test/merge_modes.c.jinja") SLUG_PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"[^-\w()]+") @classmethod def file(cls, title: str) -> Path: return Path(cls.SLUG_PATTERN.sub("-", title.lower().replace(" ", "_"))) @classmethod def _write( cls, root: Path, jinja_env: jinja2.Environment, path: Path, **kwargs, ) -> None: template_path = root / cls.c_template template = jinja_env.get_template(template_path.relative_to(root).as_posix()) with path.open("wt", encoding="utf-8") as fd: fd.writelines(template.generate(script=SCRIPT.relative_to(ROOT), **kwargs)) @classmethod def write_c( cls, root: Path, jinja_env: jinja2.Environment, path: Path, compiler: XkbCompiler, tests: Sequence[TestGroup], ) -> None: return cls._write( root=root, jinja_env=jinja_env, path=path, compiler=compiler, tests=tests ) @classmethod def write_data( cls, root: Path, compiler: XkbCompiler, tests: Sequence[TestGroup], ) -> None: tests_: dict[Path, list[Test]] = defaultdict(list) suffix = compiler.suffix for group in tests: for test in group.tests: file = test.file.parent file = file.with_name(file.name + suffix) tests_[file].append(test) for file, group_ in tests_.items(): path = root / "test" / "data" / file with path.open("wt", encoding="utf-8") as fd: fd.write( "// WARNING: This file was auto-generated by: " f"{SCRIPT.relative_to(ROOT)}\n" ) for test in group_: fd.write(test.content) @dataclass class TestTemplate(metaclass=ABCMeta): title: str _plain: ClassVar[str] = "plain" _include: ClassVar[str] = "include" _base: ClassVar[str] = "base" _update: ClassVar[str] = "update" _file: ClassVar[str] = "merge_modes" def make_title(self, name: str) -> str: return f"{self.title}: {name}" @classmethod def make_file(cls, name: str) -> Path: return TestGroup.file(name) @abstractmethod def generate_tests(self) -> TestGroup: ... @abstractmethod def generate_data(self) -> TestGroup: ... @classmethod @abstractmethod def write( cls, root: Path, jinja_env: jinja2.Environment, c_file: Path | None, xkb: bool, compiler: XkbCompiler, tests: Sequence[Self], debug: bool, ): ... @dataclass class ComponentTestTemplate(TestTemplate): component: Component # Templates to test base_template: ComponentTemplate update_template: ComponentTemplate # Expected result augment: str override: str replace: str = "" def __post_init__(self): self.augment = textwrap.dedent(self.augment).strip() self.override = textwrap.dedent(self.override).strip() if not self.replace: self.replace = self.override else: self.replace = textwrap.dedent(self.replace).strip() def expected(self, mode: MergeMode) -> str: match mode: case MergeMode.Default: return self.override case MergeMode.Augment: return self.augment case MergeMode.Override: return self.override case MergeMode.Replace: return self.replace case _: raise ValueError(mode) def _make_section_name(self, type: str, mode: MergeMode) -> str: file = self.make_file(f"{self.title}-{mode}" if self.title else mode) return f"{type}-{file}" def _make_file(self, type: str, mode: MergeMode) -> Path: section_name = self._make_section_name(type, mode) return Path(self.component) / self._file / section_name def render_section(self, content: str, name: str = "") -> str: return "\n".join(self.component.render(self.component, content, name)) def render_keymap(self, content: str) -> str: return "\n".join(self.component.render_keymap(content)) def _generate_tests( self, render: Callable[[str], str], compiler: XkbCompiler ) -> Generator[Test]: include_suffix = compiler.suffix for base_mode, update_mode in itertools.product(MergeMode, MergeMode): # Plain content = ( self.base_template.render(base_mode) + ("\n" if self.base_template else "") + self.update_template.render(update_mode) ) yield Test( title=self.make_title( f"{self._plain} ({base_mode}) - {self._plain} ({update_mode})" ), file=self.make_file(self.title) / self.make_file(update_mode), content=render(content), expected=render(self.expected(update_mode)), ) for base_mode, update_mode in itertools.product( (MergeMode.Default,), MergeMode ): # Plain + Include file = self._make_file(self._update, update_mode) section = file.name file = file.parent.name for mode in MergeMode: yield Test( title=self.make_title( f"{self._plain} ({base_mode}) - {mode.include} ({update_mode})" ), file=self.make_file(self.title) / self.make_file(mode), content=render( self.base_template.render(base_mode) + ( f'\n{mode.include} "{file}{include_suffix}({section})"' if self.base_template else "" ) ), # Include does not leak local merge modes expected=render(self.expected(mode)), ) # for base_mode, update_mode in itertools.product(MergeMode, MergeMode): # # Include + Plain # file = self._make_file(self.base, base_mode) # section = file.name # file = file.parent.name # for mode in (MergeMode.default,): # yield Test( # title=self.make_title( # f"{mode.include} ({base_mode}) - {self.plain} ({update_mode})" # ), # file=self.make_file(self.title) # / self.make_file( # f"{mode.include}({base_mode})-{self.plain}({update_mode})" # ), # content=render( # f'{mode.include} "{file}{include_suffix}({section})"\n' # + self.update_template.render(update_mode) # ), # expected=render(self.expected(update_mode)), # ) # for base_mode, update_mode in itertools.product((MergeMode.Default,), MergeMode): # # Include + Include # mode = MergeMode.Default # base_file = self._make_file(self.base, mode) # base_section = base_file.name # base_file = base_file.parent.name # update_file = self._make_file(self.update, mode) # update_section = update_file.name # update_file = update_file.parent.name # yield Test( # title=self.make_title( # f"{base_mode.include} ({mode}) - {update_mode.include} ({mode})" # ), # file=self.make_file(self.title) # / self.make_file( # f"{base_mode.include}({mode})-{update_mode.include}({mode})" # ), # content=render( # f'{base_mode.include} "{base_file}{include_suffix}({base_section})"\n' # + f'{update_mode.include} "{update_file}{include_suffix}({update_section})"' # ), # expected=render(self.expected(update_mode)), # ) for base_mode, update_mode in itertools.product(MergeMode, MergeMode): # Multi-include base_file = self._make_file(self._base, base_mode) base_section = base_file.name base_file = base_file.parent.name update_file = self._make_file(self._update, update_mode) update_section = update_file.name update_file = update_file.parent.name for mode in (MergeMode.Override, MergeMode.Augment, MergeMode.Replace): yield Test( title=self.make_title( f"{self._include} ({base_mode} {mode.char} {update_mode})" ), file=self.make_file(self.title) / self.make_file(mode), content=render( f'{base_mode.include} "{base_file}{include_suffix}({base_section})' + mode.char + f'{update_file}{include_suffix}({update_section})"' ), expected=render(self.expected(mode)), ) def _generate_data(self) -> Generator[Test]: for type, template in ( (self._base, self.base_template), (self._update, self.update_template), ): for mode in MergeMode: section_name = self._make_section_name(type, mode) file = self._make_file(type, mode) content = template.render(mode) content = self.render_section(content=content, name=section_name) + "\n" yield Test(title="", file=file, content=content, expected="") def generate_tests( self, compiler: XkbCompiler, as_keymap: bool = False ) -> TestGroup: if as_keymap: def render(content: str) -> str: return self.render_keymap(content) else: def render(content: str) -> str: return self.render_section(content) return TestGroup(self.title, tuple(self._generate_tests(render, compiler))) def generate_data(self) -> TestGroup: return TestGroup(self.title, tuple(self._generate_data())) @classmethod def write( cls, root: Path, jinja_env: jinja2.Environment, c_file: Path | None, xkb: bool, compiler: XkbCompiler, tests: Sequence[Self], debug: bool = False, ): if c_file is not None and c_file.name: c_data = tuple( t.generate_tests(as_keymap=True, compiler=compiler) for t in tests ) TestGroup.write_c( root=root, jinja_env=jinja_env, path=c_file, compiler=compiler, tests=c_data, ) if xkb: xkb_data = tuple(t.generate_data() for t in tests) TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data) KeymapTemplate = Template( """\ xkb_keymap { ${keycodes} ${types} ${compat} ${symbols} };""" ) @dataclass class KeymapTestTemplate(TestTemplate): keycodes: ComponentTestTemplate | None = None types: ComponentTestTemplate | None = None compat: ComponentTestTemplate | None = None symbols: ComponentTestTemplate | None = None def __post_init__(self): if self.keycodes: self.keycodes = dataclasses.replace(self.keycodes, title="") if self.types: self.types = dataclasses.replace(self.types, title="") if self.compat: self.compat = dataclasses.replace(self.compat, title="") if self.symbols: self.symbols = dataclasses.replace(self.symbols, title="") def __iter__(self) -> Generator[ComponentTestTemplate]: def make_empty_template(component: Component) -> ComponentTestTemplate: return ComponentTestTemplate( title="", component=component, base_template=ComponentTemplate(""), update_template=ComponentTemplate(""), augment="", override="", ) yield self.keycodes or make_empty_template(Component.keycodes) yield self.types or make_empty_template(Component.types) yield self.compat or make_empty_template(Component.compat) yield self.symbols or make_empty_template(Component.symbols) def _generate_tests(self, compiler: XkbCompiler) -> Generator[Test]: groups = tuple( c.generate_tests(as_keymap=False, compiler=compiler) for c in self ) keycodes: Test types: Test compat: Test symbols: Test for keycodes, types, compat, symbols in zip(*(g.tests for g in groups)): title = self.title + keycodes.title file = keycodes.file content = KeymapTemplate.substitute( keycodes=textwrap.indent(keycodes.content, INDENT), types=textwrap.indent(types.content, INDENT), compat=textwrap.indent(compat.content, INDENT), symbols=textwrap.indent(symbols.content, INDENT), ) expected = KeymapTemplate.substitute( keycodes=textwrap.indent(keycodes.expected, INDENT), types=textwrap.indent(types.expected, INDENT), compat=textwrap.indent(compat.expected, INDENT), symbols=textwrap.indent(symbols.expected, INDENT), ) yield Test(title=title, file=file, content=content, expected=expected) def generate_tests(self, compiler: XkbCompiler) -> TestGroup: return TestGroup(self.title, tuple(self._generate_tests(compiler))) def _generate_data(self) -> Generator[Test]: for component in self: yield from component.generate_data().tests def generate_data(self) -> TestGroup: return TestGroup(self.title, tuple(self._generate_data())) @classmethod def write( cls, root: Path, jinja_env: jinja2.Environment, c_file: Path | None, xkb: bool, compiler: XkbCompiler, tests: Sequence[Self], debug: bool = False, ): if c_file is not None and c_file.name: c_data = tuple(t.generate_tests(compiler) for t in tests) TestGroup.write_c( root=root, jinja_env=jinja_env, path=c_file, compiler=compiler, tests=c_data, ) if xkb: xkb_data = tuple(t.generate_data() for t in tests) TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data) ################################################################################ # # Key types tests # ################################################################################ TYPES_TESTS = ComponentTestTemplate( "Types", component=Component.types, base_template=ComponentTemplate( """ ${mode}virtual_modifiers NumLock, LevelThree=none, LevelFive=Mod3; ${mode}virtual_modifiers U1, U2, U3; // ${mode}virtual_modifiers U4, U5, U6; ${mode}virtual_modifiers Z1=none, Z2=none, Z3=none; // ${mode}virtual_modifiers Z4=none, Z5=none, Z6=none; ${mode}virtual_modifiers M1=0x1000, M2=0x2000, M3=0x3000; // ${mode}virtual_modifiers M4=0x4000, M5=0x5000, M6=0x6000; ${mode}type "ONE_LEVEL" { modifiers = None; map[None] = Level1; level_name[Level1]= "Any"; }; ${mode}type "TWO_LEVEL" { modifiers = Shift; map[Shift] = Level2; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; }; ${mode}type "ALPHABETIC" { modifiers = Shift+Lock; map[Shift] = Level2; map[Lock] = Level2; level_name[Level1] = "Base"; level_name[Level2] = "Caps"; }; ${mode}type "KEYPAD" { modifiers = Shift+NumLock; map[None] = Level1; map[Shift] = Level2; map[NumLock] = Level2; map[Shift+NumLock] = Level1; level_name[Level1] = "Base"; level_name[Level2] = "Number"; }; ${mode}type "FOUR_LEVEL" { modifiers = Shift+LevelThree; map[None] = Level1; map[Shift] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "FOUR_LEVEL_ALPHABETIC" { modifiers = Shift+Lock+LevelThree; map[None] = Level1; map[Shift] = Level2; map[Lock] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; map[Lock+LevelThree] = Level4; map[Lock+Shift+LevelThree] = Level3; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" { modifiers = Shift+Lock+LevelThree; map[None] = Level1; map[Shift] = Level2; map[Lock] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; map[Lock+LevelThree] = Level3; map[Lock+Shift+LevelThree] = Level4; preserve[Lock+LevelThree] = Lock; preserve[Lock+Shift+LevelThree] = Lock; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "XXX" { modifiers = Shift+Lock+LevelThree; map[None] = 1; map[Shift] = 2; map[Lock] = 2; map[LevelThree] = 3; level_name[1] = "1"; level_name[2] = "2"; level_name[3] = "3"; }; """ ), update_template=ComponentTemplate( """\ ${mode}virtual_modifiers NumLock=Mod2; // Changed: now mapped ${mode}virtual_modifiers LevelThree=Mod5; // Changed: altered mapping (from 0) ${mode}virtual_modifiers LevelFive=Mod3; // Unhanged: same mapping ${mode}virtual_modifiers U7, Z7=0, M7=0x700000; // Changed: new ${mode}virtual_modifiers U1; // Unchanged (unmapped) ${mode}virtual_modifiers U2 = none; // Changed: now mapped ${mode}virtual_modifiers U3 = 0x300000; // Changed: now mapped // ${mode}virtual_modifiers U4; // Unchanged (unmapped) // ${mode}virtual_modifiers U5 = none; // Changed: now mapped // ${mode}virtual_modifiers U6 = 0x600000; // Changed: now mapped ${mode}virtual_modifiers Z1; // Unchanged (= 0) ${mode}virtual_modifiers Z2 = none; // Unchanged (same mapping) ${mode}virtual_modifiers Z3 = 0x310000; // Changed: altered mapping (from 0) // ${mode}virtual_modifiers Z4; // Unchanged (= 0) // ${mode}virtual_modifiers Z5 = none; // Unchanged (same mapping) // ${mode}virtual_modifiers Z6 = 0x610000; // Changed: altered mapping (from 0) ${mode}virtual_modifiers M1; // Unchanged (≠ 0) ${mode}virtual_modifiers M2 = none; // Changed: reset ${mode}virtual_modifiers M3 = 0x320000; // Changed: altered mapping (from ≠ 0) // ${mode}virtual_modifiers M4; // Unchanged (≠ 0) // ${mode}virtual_modifiers M5 = none; // Changed: reset // ${mode}virtual_modifiers M6 = 0x620000; // Changed: altered mapping (from ≠ 0) ${mode}type "ONE_LEVEL" { modifiers = None; map[None] = Level1; level_name[Level1]= "New"; // Change name }; ${mode}type "TWO_LEVEL" { modifiers = Shift+M1; // Changed map[Shift] = Level2; map[M1] = Level2; // Changed level_name[Level1] = "Base"; level_name[Level2] = "Shift"; }; ${mode}type "ALPHABETIC" { modifiers = Lock; // Changed map[None] = Level2; // Changed map[Lock] = Level1; // Changed level_name[Level1] = "Base"; level_name[Level2] = "Caps"; }; ${mode}type "KEYPAD" { modifiers = Shift+NumLock; map[None] = Level1; map[Shift] = Level2; // Changed map[NumLock] = Level2; map[Shift+NumLock] = Level1; level_name[Level1] = "Base"; level_name[Level2] = "Number"; }; // Unchanged ${mode}type "FOUR_LEVEL" { modifiers = Shift+LevelThree; map[None] = Level1; map[Shift] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "FOUR_LEVEL_ALPHABETIC" { modifiers = Shift+Lock+LevelThree; map[None] = Level1; map[Shift] = Level2; map[Lock] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; map[Lock+LevelThree] = Level4; map[Lock+Shift+LevelThree] = Level3; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" { modifiers = Shift+Lock+LevelThree; map[None] = Level1; map[Shift] = Level2; map[Lock] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level4; map[Lock+LevelThree] = Level3; map[Lock+Shift+LevelThree] = Level4; preserve[Lock+LevelThree] = Lock; preserve[Lock+Shift+LevelThree] = Lock; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Alt Base"; level_name[Level4] = "Shift Alt"; }; ${mode}type "XXX" { modifiers = Shift+Lock+LevelThree; map[None] = 1; map[Shift] = 2; // map[Lock] = 2; // Changed // map[LevelThree] = 3; map[LevelThree] = 1; // Changed map[Shift+LevelThree] = 4; // Changed level_name[1] = "A"; // Changed level_name[2] = "2"; level_name[3] = "3"; level_name[4] = "4"; // Changed }; // New ${mode}type "YYY" { modifiers = None; map[None] = Level1; level_name[Level1]= "New"; }; """ ), augment=""" virtual_modifiers NumLock=Mod2,LevelThree=none,LevelFive=Mod3; virtual_modifiers U1,U2=0,U3=0x300000; // virtual_modifiers U4,U5=0,U6=0x600000; virtual_modifiers Z1=0,Z2=0,Z3=0; // virtual_modifiers Z4=0,Z5=0,Z6=0; virtual_modifiers M1=0x1000,M2=0x2000,M3=0x3000; // virtual_modifiers M4=0x4000,M5=0x5000,M6=0x6000; virtual_modifiers U7,Z7=0,M7=0x700000; type "ONE_LEVEL" { modifiers= none; map[none]= 1; level_name[1]= "Any"; }; type "TWO_LEVEL" { modifiers= Shift; map[Shift]= 2; level_name[1]= "Base"; level_name[2]= "Shift"; }; type "ALPHABETIC" { modifiers= Shift+Lock; map[Shift]= 2; map[Lock]= 2; level_name[1]= "Base"; level_name[2]= "Caps"; }; type "KEYPAD" { modifiers= Shift+NumLock; map[Shift]= 2; map[NumLock]= 2; level_name[1]= "Base"; level_name[2]= "Number"; }; type "FOUR_LEVEL" { modifiers= Shift+LevelThree; map[none]= 1; map[Shift]= 2; map[LevelThree]= 3; map[Shift+LevelThree]= 4; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "FOUR_LEVEL_ALPHABETIC" { modifiers= Shift+Lock+LevelThree; map[none]= 1; map[Shift]= 2; map[Lock]= 2; map[LevelThree]= 3; map[Shift+LevelThree]= 4; map[Lock+LevelThree]= 4; map[Shift+Lock+LevelThree]= 3; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "FOUR_LEVEL_SEMIALPHABETIC" { modifiers= Shift+Lock+LevelThree; map[None]= 1; map[Shift]= 2; map[Lock]= 2; map[LevelThree]= 3; map[Shift+LevelThree]= 4; map[Lock+LevelThree]= 3; preserve[Lock+LevelThree]= Lock; map[Shift+Lock+LevelThree]= 4; preserve[Shift+Lock+LevelThree]= Lock; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "XXX" { modifiers = Shift+Lock+LevelThree; map[None] = 1; map[Shift] = 2; map[Lock] = 2; map[LevelThree] = 3; level_name[1] = "1"; level_name[2] = "2"; level_name[3] = "3"; }; type "YYY" { modifiers = None; map[None] = Level1; level_name[Level1]= "New"; }; """, override=""" virtual_modifiers NumLock=Mod2,LevelThree=Mod5,LevelFive=Mod3; virtual_modifiers U1,U2=0,U3=0x300000; // virtual_modifiers U4,U5=0,U6=0x600000; virtual_modifiers Z1=0,Z2=0,Z3=0x310000; // virtual_modifiers Z4=0,Z5=0,Z6=0x610000; virtual_modifiers M1=0x1000,M2=0,M3=0x320000; // virtual_modifiers M4=0x4000,M5=0,M6=0x620000; virtual_modifiers U7,Z7=0,M7=0x700000; type "ONE_LEVEL" { modifiers= none; map[None] = Level1; level_name[1]= "New"; }; type "TWO_LEVEL" { modifiers= Shift+M1; map[Shift]= 2; map[M1]= 2; level_name[1]= "Base"; level_name[2]= "Shift"; }; type "ALPHABETIC" { modifiers= Lock; map[none]= 2; map[Lock]= 1; level_name[1]= "Base"; level_name[2]= "Caps"; }; type "KEYPAD" { modifiers= Shift+NumLock; map[Shift] = Level2; map[NumLock]= 2; level_name[1]= "Base"; level_name[2]= "Number"; }; type "FOUR_LEVEL" { modifiers= Shift+LevelThree; map[None]= 1; map[Shift]= 2; map[LevelThree]= 3; map[Shift+LevelThree] = Level4; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "FOUR_LEVEL_ALPHABETIC" { modifiers= Shift+Lock+LevelThree; map[Shift]= 2; map[Lock]= 2; map[LevelThree]= 3; map[Shift+LevelThree]= 4; map[Lock+LevelThree]= 4; map[Shift+Lock+LevelThree]= 3; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "FOUR_LEVEL_SEMIALPHABETIC" { modifiers= Shift+Lock+LevelThree; map[None]= 1; map[Shift]= 2; map[Lock]= 2; map[LevelThree]= 3; map[Shift+LevelThree]= 4; map[Lock+LevelThree]= 3; preserve[Lock+LevelThree]= Lock; map[Shift+Lock+LevelThree]= 4; preserve[Shift+Lock+LevelThree]= Lock; level_name[1]= "Base"; level_name[2]= "Shift"; level_name[3]= "Alt Base"; level_name[4]= "Shift Alt"; }; type "XXX" { modifiers = Shift+Lock+LevelThree; map[None] = 1; map[Shift] = 2; // map[LevelThree] = 3; map[LevelThree] = 1; map[Shift+LevelThree] = 4; level_name[1] = "A"; level_name[2] = "2"; level_name[3] = "3"; level_name[4] = "4"; }; type "YYY" { modifiers = None; map[None] = Level1; level_name[Level1]= "New"; }; """, ) ################################################################################ # # Compat tests # ################################################################################ COMPAT_TESTS = ComponentTestTemplate( "Compat", component=Component.compat, base_template=ComponentTemplate( """ ${mode}virtual_modifiers NumLock; interpret.repeat= False; setMods.clearLocks= True; latchMods.clearLocks= True; latchMods.latchToLock= True; ${mode}interpret Any + Any { action= SetMods(modifiers=modMapMods); }; ${mode}interpret Caps_Lock { action = LockMods(modifiers = Lock); }; ${mode}indicator "Caps Lock" { !allowExplicit; whichModState= Locked; modifiers= Lock; }; ${mode}indicator "Num Lock" { !allowExplicit; whichModState= Locked; modifiers= NumLock; }; """ ), update_template=ComponentTemplate( """ ${mode}virtual_modifiers NumLock; ${mode}interpret.repeat= False; ${mode}setMods.clearLocks= False; // Changed // Unchanged ${mode}interpret Any + Any { action= SetMods(modifiers=modMapMods); }; // Changed ${mode}interpret Caps_Lock { action = LockMods(modifiers = NumLock); }; // Unchanged ${mode}indicator "Caps Lock" { !allowExplicit; whichModState= Locked; modifiers= Lock; }; // Changed ${mode}indicator "Num Lock" { !allowExplicit; whichModState= Base; modifiers= Lock; }; // New ${mode}indicator "Kana" { !allowExplicit; whichModState= Locked; modifiers= Control; }; """ ), augment=""" virtual_modifiers NumLock; interpret.useModMapMods= AnyLevel; interpret.repeat= False; interpret Caps_Lock+AnyOfOrNone(all) { action= LockMods(modifiers=Lock); }; interpret Any+AnyOf(all) { action= SetMods(modifiers=modMapMods,clearLocks); }; indicator "Caps Lock" { whichModState= locked; modifiers= Lock; }; indicator "Num Lock" { whichModState= locked; modifiers= NumLock; }; indicator "Kana" { whichModState= locked; modifiers= Control; }; """, override=""" virtual_modifiers NumLock; interpret.repeat= False; setMods.clearLocks= False; interpret Caps_Lock+AnyOfOrNone(all) { action= LockMods(modifiers=NumLock); }; interpret Any+AnyOf(all) { action= SetMods(modifiers=modMapMods); }; indicator "Caps Lock" { whichModState= locked; modifiers= Lock; }; indicator "Kana" { whichModState= locked; modifiers= Control; }; indicator "Num Lock" { whichModState= base; modifiers= Lock; }; """, ) ################################################################################ # # Symbols tests # ################################################################################ LINUX_EVENT_CODES = ( None, "ESC", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "MINUS", "EQUAL", "BACKSPACE", "TAB", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "LEFTBRACE", "RIGHTBRACE", "ENTER", "LEFTCTRL", "A", "S", "D", "F", "G", "H", "J", "K", "L", "SEMICOLON", "APOSTROPHE", "GRAVE", "LEFTSHIFT", "BACKSLASH", "Z", "X", "C", "V", "B", "N", "M", "COMMA", "DOT", "SLASH", "RIGHTSHIFT", "KPASTERISK", "LEFTALT", "SPACE", "CAPSLOCK", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "NUMLOCK", "SCROLLLOCK", "KP7", "KP8", "KP9", "KPMINUS", "KP4", "KP5", "KP6", "KPPLUS", "KP1", "KP2", "KP3", "KP0", "KPDOT", None, "ZENKAKUHANKAKU", "102ND", "F11", "F12", "RO", "KATAKANA", "HIRAGANA", "HENKAN", "KATAKANAHIRAGANA", "MUHENKAN", "KPJPCOMMA", "KPENTER", "RIGHTCTRL", "KPSLASH", "SYSRQ", "RIGHTALT", "LINEFEED", "HOME", "UP", "PAGEUP", "LEFT", "RIGHT", "END", "DOWN", "PAGEDOWN", "INSERT", "DELETE", "MACRO", "MUTE", "VOLUMEDOWN", "VOLUMEUP", "POWER", "KPEQUAL", "KPPLUSMINUS", "PAUSE", "SCALE", "KPCOMMA", "HANGEUL", "HANJA", "YEN", "LEFTMETA", "RIGHTMETA", "COMPOSE", "STOP", "AGAIN", "PROPS", "UNDO", "FRONT", "COPY", "OPEN", "PASTE", "FIND", "CUT", "HELP", "MENU", "CALC", "SETUP", "SLEEP", "WAKEUP", "FILE", "SENDFILE", "DELETEFILE", "XFER", "PROG1", "PROG2", "WWW", "MSDOS", "COFFEE", "DIRECTION", "CYCLEWINDOWS", "MAIL", "BOOKMARKS", "COMPUTER", "BACK", "FORWARD", "CLOSECD", "EJECTCD", "EJECTCLOSECD", "NEXTSONG", "PLAYPAUSE", "PREVIOUSSONG", "STOPCD", "RECORD", "REWIND", "PHONE", "ISO", "CONFIG", "HOMEPAGE", "REFRESH", "EXIT", "MOVE", "EDIT", "SCROLLUP", "SCROLLDOWN", "KPLEFTPAREN", "KPRIGHTPAREN", "NEW", "REDO", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", None, None, None, None, None, "PLAYCD", "PAUSECD", "PROG3", "PROG4", "DASHBOARD", "SUSPEND", "CLOSE", "PLAY", "FASTFORWARD", "BASSBOOST", "PRINT", "HP", "CAMERA", "SOUND", "QUESTION", ) Comment = NewType("Comment", str) @dataclass(frozen=True) class KeyCode: _evdev: str _xkb: str @property def c(self) -> str: return f"KEY_{self._evdev}" @property def xkb(self) -> str: return f"<{self._xkb}>" class Keysym(str): @property def c(self) -> str: return f"XKB_KEY_{self}" @classmethod def parse(cls, raw: str | None) -> Self: if not raw: return cls("NoSymbol") else: return cls(raw) NoSymbol = Keysym("NoSymbol") class Modifier(IntFlag): NoModifier = 0 Shift = 1 << 0 Lock = 1 << 1 Control = 1 << 2 Mod1 = 1 << 3 Mod2 = 1 << 4 Mod3 = 1 << 5 Mod4 = 1 << 6 Mod5 = 1 << 7 LevelThree = Mod5 def __iter__(self) -> Iterator[Self]: for m in self.__class__: if m & self: yield m def __str__(self) -> str: return "+".join(m.name for m in self) class Action(metaclass=ABCMeta): @abstractmethod def __bool__(self) -> bool: ... @classmethod def parse(cls, raw: Any) -> Self: if raw is None: return GroupAction.parse(None) elif isinstance(raw, Modifier): return ModAction.parse(raw) elif isinstance(raw, int): return GroupAction.parse(raw) else: raise ValueError(raw) @abstractmethod def action_to_keysym(self, index: int, level: int) -> Keysym: ... @dataclass class GroupAction(Action): """ SetGroup or NoAction """ group: int keysyms: ClassVar[dict[tuple[int, int, int], Keysym]] = { (2, 0, 0): Keysym("a"), (2, 0, 1): Keysym("A"), (2, 1, 0): Keysym("b"), (2, 1, 1): Keysym("B"), (3, 0, 0): Keysym("Greek_alpha"), (3, 0, 1): Keysym("Greek_ALPHA"), (3, 1, 0): Keysym("Greek_beta"), (3, 1, 1): Keysym("Greek_BETA"), } def __str__(self) -> str: if self.group > 0: return f"SetGroup(group={self.group})" else: return "NoAction()" def __bool__(self) -> bool: return bool(self.group) @classmethod def parse(cls, raw: int | None) -> Self: if not raw: return cls(0) else: return cls(raw) def action_to_keysym(self, index: int, level: int) -> Keysym: if not self.group: return NoSymbol else: if ( keysym := self.keysyms.get((self.group % 4, index % 2, level % 2)) ) is None: raise ValueError((self, index, level)) return keysym @dataclass class ModAction(Action): """ SetMod or NoAction """ mods: Modifier keysyms: ClassVar[dict[tuple[Modifier, int, int], Keysym]] = { (Modifier.Control, 0, 0): Keysym("x"), (Modifier.Control, 0, 1): Keysym("X"), (Modifier.Control, 1, 0): Keysym("y"), (Modifier.Control, 1, 1): Keysym("Y"), (Modifier.Mod5, 0, 0): Keysym("Greek_xi"), (Modifier.Mod5, 0, 1): Keysym("Greek_XI"), (Modifier.Mod5, 1, 0): Keysym("Greek_upsilon"), (Modifier.Mod5, 1, 1): Keysym("Greek_UPSILON"), } def __str__(self) -> str: if self.mods is Modifier.NoModifier: return "NoAction()" else: return f"SetMods(mods={self.mods})" def __bool__(self) -> bool: return self.mods is Modifier.NoModifier @classmethod def parse(cls, raw: Modifier | None) -> Self: if not raw: return cls(Modifier.NoModifier) else: return cls(raw) def action_to_keysym(self, index: int, level: int) -> Keysym: if self.mods is Modifier.NoModifier: return NoSymbol else: if (keysym := self.keysyms.get((self.mods, index % 2, level % 2))) is None: raise ValueError((self, index, level)) return keysym @dataclass class Level: keysyms: tuple[Keysym, ...] actions: tuple[Action, ...] @staticmethod def _c(default: str, xs: Iterable[Any]) -> str: match len(xs): case 0: return default case 1: return xs[0].c case _: return ", ".join(map(lambda x: x.c, xs)) @staticmethod def _xkb(default: str, xs: Iterable[Any]) -> str: match len(xs): case 0: return default case 1: return str(xs[0]) case _: return "{" + ", ".join(map(str, xs)) + "}" @classmethod def has_empty_symbols(cls, keysyms: tuple[Keysym, ...]) -> bool: return all(ks == NoSymbol for ks in keysyms) @property def empty_symbols(self) -> bool: return self.has_empty_symbols(self.keysyms) @property def keysyms_c(self) -> str: if not self.keysyms and self.actions: return self._c( NoSymbol.c, tuple(itertools.repeat(NoSymbol, len(self.actions))) ) return self._c(NoSymbol.c, self.keysyms) @property def keysyms_xkb(self) -> str: return self._xkb(NoSymbol, self.keysyms) @classmethod def has_empty_actions(cls, actions: tuple[Action, ...]) -> bool: return not any(actions) @property def empty_actions(self) -> bool: return self.has_empty_actions(self.actions) @property def actions_xkb(self) -> str: return self._xkb("NoAction()", self.actions) @classmethod def Keysyms(cls, *keysyms: str | None) -> Self: return cls.Mix(keysyms, ()) @classmethod def Actions(cls, *actions: int | Modifier | None) -> Self: return cls.Mix((), actions) @classmethod def Mix( cls, keysyms: tuple[str | None, ...], actions: tuple[int | Modifier | None,] ) -> Self: return cls(tuple(map(Keysym.parse, keysyms)), tuple(map(Action.parse, actions))) def add_keysyms(self, keep_actions: bool, level: int) -> Self: return self.__class__( keysyms=tuple( a.action_to_keysym(index=k, level=level) for k, a in enumerate(self.actions) ), actions=self.actions if keep_actions else (), ) @property def target_group(self) -> int: for a in self.actions: if isinstance(a, GroupAction) and a.group > 1: return a.group else: return 0 @property def target_level(self) -> int: for a in self.actions: if isinstance(a, ModAction) and a.mods: match a.mods: case Modifier.LevelThree: return 2 case _: return 0 else: return 0 @dataclass class KeyEntry: levels: tuple[Level, ...] def __init__(self, *levels: Level): self.levels = levels @property def xkb(self) -> Iterator[str]: if not self.levels: yield "" return keysyms = tuple(l.keysyms for l in self.levels) has_keysyms = any(not Level.has_empty_symbols(s) for s in keysyms) no_keysyms = all(not s for s in keysyms) actions = tuple(l.actions for l in self.levels) has_actions = any(not Level.has_empty_actions(a) for a in actions) if has_keysyms or (not no_keysyms and not has_actions): yield "[" yield ", ".join(l.keysyms_xkb for l in self.levels) yield "]" if has_actions or no_keysyms: if has_keysyms: yield ", " yield "[" yield ", ".join(l.actions_xkb for l in self.levels) yield "]" def add_keysyms(self, keep_actions: bool) -> Self: return self.__class__( *( l.add_keysyms(keep_actions=keep_actions, level=k) for k, l in enumerate(self.levels) ) ) class TestType(IntFlag): KeysymsOnly = auto() ActionsOnly = auto() KeysymsAndActions = auto() All = ActionsOnly | KeysymsOnly | KeysymsAndActions @dataclass class TestId: type: TestType base: int = 0 _last_base: ClassVar[int] = 0 _max_type: ClassVar[int] = TestType.KeysymsAndActions >> 1 _forbidden_keys: tuple[str, ...] = ("LEFTSHIFT", "RIGHTALT") def __post_init__(self): if self.base == -1: self.base = self.__class__._last_base elif self.base < 0: raise ValueError(self.base) elif self.base == 0: self.__class__._last_base += 1 self.base = self.__class__._last_base while not self.check_linux_keys(self._base_id): self.__class__._last_base += 1 self.base = self.__class__._last_base else: self.__class__._last_base = max(self.__class__._last_base, self.base) assert self.linux_key, self # print(self.base, self.id, self.xkb_key) def __hash__(self) -> int: return self.id @property def _base_id(self) -> int: return (self.base - 1) * (self._max_type + 1) + 1 @property def id(self) -> int: # return (self.base << 2) | (self.type >> 1) return self._base_id + (self.type >> 1) def with_type(self, type: TestType) -> Self: return dataclasses.replace(self, type=type) @property def xkb_key(self) -> str: return f"T{self.id:0>3}" @classmethod def check_linux_key(cls, idx: int) -> bool: if idx >= len(LINUX_EVENT_CODES): raise ValueError("Not enough Linux keys available!") key: str | None = LINUX_EVENT_CODES[idx] return bool(key) and key not in cls._forbidden_keys @classmethod def check_linux_keys(cls, base_id) -> bool: return all( cls.check_linux_key(base_id + k) for k in range(0, cls._max_type + 1) ) @property def linux_key(self) -> str: if self.id >= len(LINUX_EVENT_CODES): raise ValueError("Not enough Linux keys available!") return LINUX_EVENT_CODES[self.id] @property def key(self) -> KeyCode: return KeyCode(self.linux_key, self.xkb_key) @unique class Implementation(Flag): x11 = auto() xkbcommon = auto() all = x11 | xkbcommon @dataclass class TestEntry: id: TestId key: KeyCode = dataclasses.field(init=False) base: KeyEntry update: KeyEntry augment: KeyEntry override: KeyEntry replace: KeyEntry types: TestType implementations: Implementation group_keysyms: ClassVar[tuple[tuple[str, str], ...]] = ( ("Ukrainian_i", "Ukrainian_I", "Ukrainian_yi", "Ukrainian_YI"), ("ch", "Ch", "c_h", "C_h"), ) def __init__( self, id: TestId | None, base: KeyEntry, update: KeyEntry, augment: KeyEntry, override: KeyEntry, types: TestType = TestType.All, replace: KeyEntry | None = None, implementations: Implementation = Implementation.all, ): self.id = TestId(0, 0) if id is None else id self.key = id.key self.base = base self.update = update self.augment = augment self.override = override self.types = types self.replace = self.update if replace is None else replace self.implementations = implementations @property def default(self) -> KeyEntry: return self.override def add_keysyms(self, keep_actions: bool) -> Self: types = TestType.KeysymsAndActions if keep_actions else TestType.KeysymsOnly return dataclasses.replace( self, id=self.id.with_type(types), base=self.base.add_keysyms(keep_actions=keep_actions), update=self.update.add_keysyms(keep_actions=keep_actions), augment=self.augment.add_keysyms(keep_actions=keep_actions), override=self.override.add_keysyms(keep_actions=keep_actions), replace=self.replace.add_keysyms(keep_actions=keep_actions), types=types, ) @classmethod def alt_keysym(cls, group: int, level: int) -> Keysym: return Keysym(cls.group_keysyms[group % 2][level % 4]) @classmethod def alt_keysyms(cls, group: int) -> Iterator[Keysym]: for keysym in cls.group_keysyms[group % 2]: yield Keysym(keysym) @dataclass class SymbolsTestGroup: name: str tests: tuple[TestEntry | Comment, ...] def _with_implementation( self, implementation: Implementation ) -> Iterable[TestEntry | Comment]: pending_comment: Comment | None = None for t in self.tests: if not isinstance(t, TestEntry): pending_comment = t elif t.implementations & implementation: if pending_comment is not None: yield pending_comment pending_comment = None yield t def with_implementation(self, implementation: Implementation) -> Self: return dataclasses.replace( self, tests=tuple(self._with_implementation(implementation)) ) @staticmethod def _add_keysyms(entry: TestEntry | Comment) -> Iterable[TestEntry | Comment]: if isinstance(entry, TestEntry) and entry.id.type is TestType.ActionsOnly: if entry.types & TestType.KeysymsOnly: yield entry.add_keysyms(keep_actions=False) yield entry if entry.types & TestType.KeysymsAndActions: yield entry.add_keysyms(keep_actions=True) else: yield entry def add_keysyms(self, name: str = "") -> Self: return dataclasses.replace( self, name=name or self.name, tests=tuple(t for ts in self.tests for t in self._add_keysyms(ts)), ) def __add__(self, other: Any) -> Self: if isinstance(other, tuple): return dataclasses.replace(self, tests=self.tests + other) elif isinstance(other, self.__class__): return dataclasses.replace(self, tests=self.tests + other.tests) else: return NotImplemented def get_keycodes(self, base: ComponentTestTemplate) -> ComponentTestTemplate: ids: set[TestId] = set(t.id for t in self.tests if isinstance(t, TestEntry)) base_template = Template( "\n".join(f"${{mode}}<{id.xkb_key}> = {id.id + 8};" for id in ids) + "\n" ) keycodes = base_template.substitute(mode="") return dataclasses.replace( base, base_template=base_template + base.base_template, update_template=base.update_template, augment=keycodes + base.augment, override=keycodes + base.override, replace=keycodes + base.replace, ) def _get_symbols(self, type: str, debug: bool) -> Generator[str]: for entry in self.tests: if isinstance(entry, str): if type == "update": yield f"\n////// {entry} //////" else: data: KeyEntry = getattr(entry, type) if debug: yield f"// base: {''.join(entry.base.xkb) or '(empty)'}" yield "// key to merge / replace result" yield f"${{mode}}key {entry.key.xkb} {{ {''.join(data.xkb)} }};" if debug: yield f"// override: {''.join(entry.override.xkb) or '(empty)'}" yield f"// augment: {''.join(entry.augment.xkb) or '(empty)'}" def get_symbols(self, debug: bool = False) -> ComponentTestTemplate: base_template = ComponentTemplate("\n".join(self._get_symbols("base", False))) update_template = ComponentTemplate( "\n".join(self._get_symbols("update", debug)) ) augment = ComponentTemplate( "\n".join(self._get_symbols("augment", False)) ).substitute(mode="") override = ComponentTemplate( "\n".join(self._get_symbols("override", False)) ).substitute(mode="") replace = ComponentTemplate( "\n".join(self._get_symbols("replace", False)) ).substitute(mode="") return ComponentTestTemplate( title=self.name, component=Component.symbols, base_template=base_template, update_template=update_template, augment=augment, override=override, replace=replace, ) SYMBOLS_TESTS_BOTH = SymbolsTestGroup( "", ( Comment("Trivial cases"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(), update=KeyEntry(), augment=KeyEntry(), override=KeyEntry(), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(), update=KeyEntry(Level.Actions(3)), augment=KeyEntry(Level.Actions(3)), override=KeyEntry(Level.Actions(3)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2)), update=KeyEntry(), augment=KeyEntry(Level.Actions(2)), override=KeyEntry(Level.Actions(2)), ), Comment("Same key"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2)), update=KeyEntry(Level.Actions(2)), augment=KeyEntry(Level.Actions(2)), override=KeyEntry(Level.Actions(2)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), override=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), implementations=Implementation.xkbcommon, ), Comment("Mismatch levels count"), ( TEST_BOTH_Q := TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(2)), update=KeyEntry( Level.Actions(3), Level.Actions(None), Level.Actions(None) ), augment=KeyEntry( Level.Actions(3), Level.Actions(2), Level.Actions(None) ), override=KeyEntry( Level.Actions(3), Level.Actions(2), Level.Actions(None) ), # X11 and xkbcommon handle keysyms-only case differently types=TestType.ActionsOnly | TestType.KeysymsAndActions, ) ), # Trailing NoSymbols are discarded in xkbcomp dataclasses.replace( TEST_BOTH_Q, augment=KeyEntry(Level.Actions(3), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(2)), replace=KeyEntry(Level.Actions(3)), implementations=Implementation.x11, ).add_keysyms(keep_actions=False), dataclasses.replace( TEST_BOTH_Q, implementations=Implementation.xkbcommon ).add_keysyms(keep_actions=False), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Actions(None), Level.Actions(None)), augment=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)), override=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)), # X11 and xkbcommon handle keysyms-only case differently types=TestType.ActionsOnly | TestType.KeysymsAndActions, ), ( TEST_BOTH_W := TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(2), Level.Actions(None)), update=KeyEntry(Level.Actions(3), Level.Actions(None)), augment=KeyEntry( Level.Actions(3), Level.Actions(2), Level.Actions(None) ), override=KeyEntry( Level.Actions(3), Level.Actions(2), Level.Actions(None) ), # X11 and xkbcommon handle keysyms-only case differently types=TestType.ActionsOnly | TestType.KeysymsAndActions, ) ), dataclasses.replace( TEST_BOTH_W, augment=KeyEntry(Level.Actions(3), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(2)), replace=KeyEntry(Level.Actions(3)), implementations=Implementation.x11, ).add_keysyms(keep_actions=False), dataclasses.replace( TEST_BOTH_W, implementations=Implementation.xkbcommon ).add_keysyms(keep_actions=False), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(3)), override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(2)), ), Comment("Single keysyms -> single keysyms"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(None), Level.Actions(None)), augment=KeyEntry(Level.Actions(None), Level.Actions(None)), override=KeyEntry(Level.Actions(None), Level.Actions(None)), ), ( TEST_BOTH_Y := TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(3), Level.Actions(None)), augment=KeyEntry(Level.Actions(3), Level.Actions(None)), override=KeyEntry(Level.Actions(3), Level.Actions(None)), # X11 and xkbcommon handle keysyms-only case differently types=TestType.ActionsOnly | TestType.KeysymsAndActions, ) ), dataclasses.replace( TEST_BOTH_Y, augment=KeyEntry(Level.Actions(3)), override=KeyEntry(Level.Actions(3)), replace=KeyEntry(Level.Actions(3)), implementations=Implementation.x11, ).add_keysyms(keep_actions=False), dataclasses.replace( TEST_BOTH_Y, implementations=Implementation.xkbcommon ).add_keysyms(keep_actions=False), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(None), Level.Actions(3)), augment=KeyEntry(Level.Actions(None), Level.Actions(3)), override=KeyEntry(Level.Actions(None), Level.Actions(3)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(3), Level.Actions(3)), override=KeyEntry(Level.Actions(3), Level.Actions(3)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(None), Level.Actions(None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(2), Level.Actions(2)), ), ( TEST_BOTH_P := TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Actions(None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(2)), # X11 and xkbcommon handle keysyms-only case differently types=TestType.ActionsOnly | TestType.KeysymsAndActions, ) ), dataclasses.replace( TEST_BOTH_P, augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(2)), replace=KeyEntry(Level.Actions(3)), implementations=Implementation.x11, ).add_keysyms(keep_actions=False), dataclasses.replace( TEST_BOTH_P, implementations=Implementation.xkbcommon ).add_keysyms(keep_actions=False), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(None), Level.Actions(3)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(2), Level.Actions(3)), ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3), Level.Actions(3)), ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a"), Level.Actions(2)), update=KeyEntry(Level.Actions(3), Level.Keysyms("X")), augment=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))), override=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))), ), Comment("Single keysyms -> multiple keysyms"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None)), augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)), override=KeyEntry(Level.Actions(3, None), Level.Actions(None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)), augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)), override=KeyEntry(Level.Actions(3, None), Level.Actions(None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)), override=KeyEntry(Level.Actions(None), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)), override=KeyEntry(Level.Actions(None), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)), override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None), Level.Actions(None)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), augment=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), override=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3, None), Level.Actions(2)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3, None), Level.Actions(2)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(2), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(2), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2), Level.Actions(2)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), augment=KeyEntry(Level.Actions(2), Level.Actions(2)), override=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), implementations=Implementation.xkbcommon, ), Comment("Multiple keysyms -> multiple keysyms"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(None, None)), update=KeyEntry( Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree) ), augment=KeyEntry( Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree) ), override=KeyEntry( Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree) ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(None, None)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), augment=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), augment=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(2, Modifier.Control), ), override=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), augment=KeyEntry( Level.Actions(2, None), Level.Actions(None, 2), ), override=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control) ), update=KeyEntry( Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree) ), augment=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control) ), override=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(3, Modifier.LevelThree), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2) ), update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), augment=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2) ), override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)), update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)), augment=KeyEntry( Level.Actions(None, None), Level.Actions(None, None, None) ), override=KeyEntry( Level.Actions(None, None), Level.Actions(None, None, None) ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)), update=KeyEntry( Level.Actions(3, None, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), augment=KeyEntry( Level.Actions(3, None, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), override=KeyEntry( Level.Actions(3, None, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, None, 2), ), update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)), augment=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, None, 2), ), override=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, None, 2), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, None, 2), ), update=KeyEntry( Level.Actions(3, None, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), augment=KeyEntry( Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, None, 2), ), override=KeyEntry( Level.Actions(3, None, Modifier.LevelThree), Level.Actions(Modifier.LevelThree, 3), ), implementations=Implementation.xkbcommon, ), Comment("Multiple keysyms -> single keysyms"), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(None), Level.Actions(None)), augment=KeyEntry( Level.Actions(None, None), Level.Actions(2, Modifier.Control) ), override=KeyEntry( Level.Actions(None, None), Level.Actions(2, Modifier.Control) ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(3), Level.Actions(2, Modifier.Control)), override=KeyEntry(Level.Actions(3), Level.Actions(3)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), update=KeyEntry(Level.Actions(None), Level.Actions(None)), augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), override=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.ActionsOnly), KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), update=KeyEntry(Level.Actions(3), Level.Actions(3)), augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)), override=KeyEntry(Level.Actions(3), Level.Actions(3)), implementations=Implementation.xkbcommon, ), Comment("Mix"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Actions(2)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), augment=KeyEntry(Level.Actions(2), Level.Actions(3, Modifier.LevelThree)), override=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3, Modifier.LevelThree), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)), augment=KeyEntry(Level.Actions(2, Modifier.Control), Level.Actions(3)), override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)), augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)), override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)), implementations=Implementation.xkbcommon, ), Comment("Mix"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a"), Level.Actions(2)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y") ), augment=KeyEntry( Level.Mix(("a",), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2,)), ), override=KeyEntry( Level.Mix(("a",), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2,)), ), implementations=Implementation.xkbcommon, ), Comment("Multiple keysyms/actions –> single"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)), update=KeyEntry(Level.Actions(3), Level.Keysyms("X")), augment=KeyEntry( Level.Mix(("a", "b"), (3,)), Level.Mix(("X",), (2, Modifier.Control)), ), override=KeyEntry( Level.Mix(("a", "b"), (3,)), Level.Mix(("X",), (2, Modifier.Control)), ), implementations=Implementation.xkbcommon, ), Comment("Multiple keysyms/actions –> multiple (xor)"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y") ), augment=KeyEntry( Level.Mix(("a", "b"), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2, Modifier.Control)), ), override=KeyEntry( Level.Mix(("a", "b"), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2, Modifier.Control)), ), implementations=Implementation.xkbcommon, ), Comment("Multiple keysyms/actions –> multiple (mix)"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a", None), Level.Actions(2, None)), update=KeyEntry( Level.Mix(("x", "y"), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (3, Modifier.LevelThree)), ), augment=KeyEntry( Level.Mix(("a", None), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2, None)), ), override=KeyEntry( Level.Mix(("x", "y"), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (3, Modifier.LevelThree)), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)), update=KeyEntry( Level.Mix(("x", None), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (3, None)), ), augment=KeyEntry( Level.Mix(("a", "b"), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (2, Modifier.Control)), ), override=KeyEntry( Level.Mix(("x", None), (3, Modifier.LevelThree)), Level.Mix(("X", "Y"), (3, None)), ), implementations=Implementation.xkbcommon, ), Comment("Multiple (mix) –> multiple keysyms/actions"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Mix(("a", "b"), (2, Modifier.Control)), Level.Mix(("A", "B"), (2, Modifier.Control)), ), update=KeyEntry(Level.Keysyms("x", None), Level.Actions(3, None)), augment=KeyEntry( Level.Mix(("a", "b"), (2, Modifier.Control)), Level.Mix(("A", "B"), (2, Modifier.Control)), ), override=KeyEntry( Level.Mix(("x", None), (2, Modifier.Control)), Level.Mix(("A", "B"), (3, None)), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Mix(("a", None), (2, Modifier.Control)), Level.Mix(("A", "B"), (2, None)), ), update=KeyEntry( Level.Keysyms("x", "y"), Level.Actions(3, Modifier.LevelThree) ), augment=KeyEntry( Level.Mix(("a", None), (2, Modifier.Control)), Level.Mix(("A", "B"), (2, None)), ), override=KeyEntry( Level.Mix(("x", "y"), (2, Modifier.Control)), Level.Mix(("A", "B"), (3, Modifier.LevelThree)), ), implementations=Implementation.xkbcommon, ), Comment("Multiple (mix) –> multiple (mix)"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Mix(("a", "b"), (2, Modifier.Control)), Level.Mix((None, "B"), (2, None)), ), update=KeyEntry( Level.Mix((None, "y"), (3, None)), Level.Mix(("X", "Y"), (3, Modifier.LevelThree)), ), augment=KeyEntry( Level.Mix(("a", "b"), (2, Modifier.Control)), Level.Mix((None, "B"), (2, None)), ), override=KeyEntry( Level.Mix((None, "y"), (3, None)), Level.Mix(("X", "Y"), (3, Modifier.LevelThree)), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Mix(("a", None), (2, None)), Level.Mix((None, "B"), (None, Modifier.Control)), ), update=KeyEntry( Level.Mix((None, "y"), (None, Modifier.LevelThree)), Level.Mix(("X", None), (3, None)), ), augment=KeyEntry( Level.Mix(("a", None), (2, None)), Level.Mix((None, "B"), (None, Modifier.Control)), ), override=KeyEntry( Level.Mix((None, "y"), (None, Modifier.LevelThree)), Level.Mix(("X", None), (3, None)), ), implementations=Implementation.xkbcommon, ), Comment("Mismatch count with mix"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Keysyms("a"), Level.Keysyms("A", "B"), ), update=KeyEntry( Level.Actions(3, Modifier.LevelThree), Level.Actions(3), ), augment=KeyEntry( Level.Mix(("a"), (3, Modifier.LevelThree)), Level.Mix(("A", "B"), (3,)), ), override=KeyEntry( Level.Mix(("a"), (3, Modifier.LevelThree)), Level.Mix(("A", "B"), (3,)), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Actions(3), Level.Actions(3, Modifier.LevelThree), ), update=KeyEntry( Level.Keysyms("A", "B"), Level.Keysyms("a"), ), augment=KeyEntry( Level.Mix(("A", "B"), (3,)), Level.Mix(("a"), (3, Modifier.LevelThree)), ), override=KeyEntry( Level.Mix(("A", "B"), (3,)), Level.Mix(("a"), (3, Modifier.LevelThree)), ), implementations=Implementation.xkbcommon, ), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry( Level.Mix(("a",), (2,)), Level.Mix(("A", "B"), (2, Modifier.Control)), ), update=KeyEntry( Level.Mix(("x", "y"), (3, Modifier.LevelThree)), Level.Mix(("X",), (3,)), ), augment=KeyEntry( Level.Mix(("a",), (2,)), Level.Mix(("A", "B"), (2, Modifier.Control)), ), override=KeyEntry( Level.Mix(("x", "y"), (3, Modifier.LevelThree)), Level.Mix(("X",), (3,)), ), implementations=Implementation.xkbcommon, ), Comment("Issue #564"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Keysyms("A")), update=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))), augment=KeyEntry(Level.Mix(("A",), (3, Modifier.LevelThree))), override=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))), implementations=Implementation.xkbcommon, ), Comment("Drop NoSymbol/NoAction"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Mix(("A",), (2,))), update=KeyEntry(Level.Mix((None, "Y", None), (None, 3, None))), augment=KeyEntry(Level.Mix(("A",), (2,))), override=KeyEntry(Level.Mix(("Y",), (3,))), implementations=Implementation.xkbcommon, ), Comment("Drop NoSymbol/NoAction and invalid keysyms"), TestEntry( TestId(TestType.KeysymsAndActions), KeyEntry(Level.Mix(("notAKeysym", None, "thisEither"), (None, None))), update=KeyEntry(Level.Mix((None, None), (None, None))), augment=KeyEntry(Level.Keysyms(None)), override=KeyEntry(Level.Keysyms(None)), replace=KeyEntry(Level.Keysyms(None)), implementations=Implementation.xkbcommon, ), ), ).add_keysyms() ################################################################################ # # Keycodes tests # ################################################################################ KEYCODES_TESTS_BASE = ComponentTestTemplate( "Keycodes", component=Component.keycodes, base_template=ComponentTemplate( """ ${mode}<1> = 241; ${mode}<2> = 242; ${mode}alias = <1>; ${mode}alias = <2>; ${mode}indicator 1 = "Caps Lock"; ${mode}indicator 2 = "Num Lock"; ${mode}indicator 3 = "Scroll Lock"; ${mode}indicator 4 = "Compose"; ${mode}indicator 5 = "Kana"; """ ), update_template=ComponentTemplate( """ ${mode}<1> = 241; // Unchanged ${mode}<2> = 244; // Changed ${mode}<3> = 243; // New ${mode}alias = <1>; // Unchanged ${mode}alias = <3>; // Changed ${mode}alias = <3>; // New ${mode}indicator 1 = "Caps Lock"; // Unchanged ${mode}indicator 6 = "Num Lock"; // Changed index (free) ${mode}indicator 5 = "Scroll Lock"; // Changed index (not free) ${mode}indicator 4 = "XXXX"; // Changed name ${mode}indicator 7 = "Suspend"; // New """ ), augment=""" <1> = 241; <2> = 242; <3> = 243; alias = <1>; alias = <2>; alias = <3>; indicator 1 = "Caps Lock"; indicator 2 = "Num Lock"; indicator 3 = "Scroll Lock"; indicator 4 = "Compose"; indicator 5 = "Kana"; indicator 7 = "Suspend"; """, override=""" <1> = 241; <2> = 244; <3> = 243; alias = <1>; alias = <3>; alias = <3>; indicator 1 = "Caps Lock"; indicator 4 = "XXXX"; indicator 5 = "Scroll Lock"; indicator 6 = "Num Lock"; indicator 7 = "Suspend"; """, ) ################################################################################ # # Main # ################################################################################ if __name__ == "__main__": # Parse commands parser = argparse.ArgumentParser(description="Generate merge mode tests") parser.add_argument( "compiler", type=XkbCompiler.parse, nargs="?", default="xkbcommon", help="XKB compiler to use", ) parser.add_argument( "--root", type=Path, default=ROOT, help="Path to the root of the project (default: %(default)s)", ) parser.add_argument( "--c-out", type=Path, # default=ROOT / TestGroup.c_template.stem, help="Path to the output C test file (default: %(default)s)", ) parser.add_argument("--no-xkb", action="store_true", help="Do not update XKB data") parser.add_argument("--debug", action="store_true", help="Activate debug mode") args = parser.parse_args() # Prepare data template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8") jinja_env = jinja2.Environment( loader=template_loader, keep_trailing_newline=True, trim_blocks=True, lstrip_blocks=True, ) def escape_quotes(s: str) -> str: return s.replace('"', '\\"') jinja_env.filters["escape_quotes"] = escape_quotes compiler: XkbCompiler = args.compiler SYMBOLS_TESTS = SYMBOLS_TESTS_BOTH.with_implementation( Implementation.xkbcommon if compiler.extended_syntax else Implementation.x11 ) KEYCODES_TESTS = SYMBOLS_TESTS.get_keycodes(KEYCODES_TESTS_BASE) TESTS = ( KeymapTestTemplate( title="", keycodes=KEYCODES_TESTS, types=TYPES_TESTS, compat=COMPAT_TESTS, symbols=SYMBOLS_TESTS.get_symbols(debug=args.debug), ), ) # Write tests TESTS[0].write( root=args.root, jinja_env=jinja_env, c_file=args.c_out, xkb=not args.no_xkb, compiler=compiler, tests=TESTS, )