#!/usr/bin/env python3 """ Generate C file to handle keysym names """ import argparse import itertools import random import sys from pathlib import Path from typing import Generator, Iterable, Iterator import perfect_hash # Root of the project SCRIPT = Path(__file__) ROOT = SCRIPT.parent.parent sys.path.append(str(SCRIPT.parent)) from keysyms import Keysym, Keysyms, Semantics # noqa: E402 # libxkbcommon Keysyms names header KEYSYMS_NAMES_HEADER = ROOT / "src" / "keysym-names.h" if not KEYSYMS_NAMES_HEADER.is_file(): raise FileNotFoundError(KEYSYMS_NAMES_HEADER) # Parse commands parser = argparse.ArgumentParser(description="Generate C file to handle keysym names") parser.add_argument( "c_header", type=Path, help="Path to the libxkbcommon keysym header" ) parser.add_argument("gperf", type=Path, help="Path to the gperf file") args = parser.parse_args() # Set the seed explicitly, so we reduce diff random.seed(b"libxkbcommon") all_keysyms = Keysyms.parse_file(args.c_header) entries: tuple[Keysym, ...] = tuple( itertools.chain.from_iterable(all_keysyms.by_value.values()) ) # Sort based on the keysym name: # 1. Sort by the casefolded name: e.g. kana_ya < kana_YO. # 2. If same casefolded name, then sort by cased name, i.e for # ASCII: upper before lower: e.g kana_YA < kana_ya. # E.g. kana_YA < kana_ya < kana_YO < kana_yo # WARNING: this sort must not be changed, as some functions e.g. # xkb_keysym_from_name rely on upper case variant occuring first. entries_isorted = sorted(entries, key=lambda e: (e.name.casefold(), e.name)) # Sort based on keysym value. Sort is stable so in case of duplicate, the first # keysym occurence stays first. entries_kssorted = sorted(entries, key=lambda e: e.value) print( f""" /** * This file comes from libxkbcommon and was generated by {SCRIPT.name} * You can always fetch the latest version from: * https://raw.github.com/xkbcommon/libxkbcommon/master/{KEYSYMS_NAMES_HEADER.relative_to(ROOT)} */ #pragma once """ ) entry_offsets: dict[str, int] = {} UINT16_MAX = (1 << 16) - 1 UNICODE_KEYSYM = UINT16_MAX - 1 DEPRECATED_KEYSYM = UINT16_MAX MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2 = 8 MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX = 1 << MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2 MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2 = 4 MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT = 1 << MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2 MAX_OFFSET = UNICODE_KEYSYM - 1 XKB_KEYSYM_UNICODE_MIN = 0x01000100 XKB_KEYSYM_UNICODE_MAX = 0x0110FFFF print( """ #include "config.h" #include #include #include "xkbcommon/xkbcommon.h" #include "utils.h" #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Woverlength-strings" #endif static const char *keysym_names = """.strip() ) offs = 0 for keysym in entries_isorted: if offs >= MAX_OFFSET: raise ValueError(f"Offset must be kept under {MAX_OFFSET}, got: {offs}.") entry_offsets[keysym.name] = offs print(f' "{keysym.name}\\0"') offs += len(keysym.name) + 1 print( """ ; #ifdef __GNUC__ #pragma GCC diagnostic pop #endif """.strip() ) template = r""" static const uint16_t keysym_name_G[] = { $G }; static inline size_t keysym_name_perfect_hash(const char *key) { const char *T1 = "$S1"; const char *T2 = "$S2"; size_t h1 = 0; size_t h2 = 0; for (size_t i = 0; key[i] != '\0'; i++) { h1 += (size_t) (T1[i % $NS] * key[i]); h2 += (size_t) (T2[i % $NS] * key[i]); } return (keysym_name_G[h1 % $NG] + keysym_name_G[h2 % $NG]) % $NG; } """ print( perfect_hash.generate_code( keys=[keysym.name for keysym in entries_isorted], template=template, ) ) print( """ struct name_keysym { xkb_keysym_t keysym; uint16_t offset; };\n""" ) def print_entries(entries: Iterable[Keysym]): for entry in entries: print( " {{ 0x{value:08x}, {offs} }}, /* {name} */".format( offs=entry_offsets[entry.name], value=entry.value, name=entry.name ) ) print("static const struct name_keysym name_to_keysym[] = {") print_entries(entries_isorted) print("};\n") # *.sort() is stable so we always get the first keysym for duplicate print("static const struct name_keysym keysym_to_name[] = {") print_entries( next(g[1]) for g in itertools.groupby(entries_kssorted, key=lambda e: e.value) ) print("};\n") def make_deprecated_entry( value, keysyms: list[Keysym], entry_offsets: dict[str, int], explicit_deprecated_aliases_index: int, ) -> tuple[str | None, tuple[int, ...]]: assert keysyms if all(not k.deprecated for k in keysyms): # No name is deprecated return None, () canonical = keysyms[0] assert canonical.is_canonical ref = canonical.preferred non_deprecated_names = tuple(k for k in keysyms if not k.deprecated) deprecated_names = tuple(k for k in keysyms if k.deprecated) deprecated_names_indices: tuple[int, ...] = () if non_deprecated_names: # Keysym is not deprecated, but some of its names are. assert not ref.deprecated, ref ref_name = f"Reference: {ref.name}. " ref_index = str(entry_offsets[ref.name]) assert deprecated_names or ref.deprecated, keysyms if any(k is not ref for k in non_deprecated_names): # Keysym has both multiple valid names and some deprecated names deprecated_names_indices = tuple( entry_offsets[k.name] for k in deprecated_names ) assert ( explicit_deprecated_aliases_index < MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX ) assert len(deprecated_names_indices) < MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT else: # Keysym has a single valid name and some deprecated names assert len(non_deprecated_names) == 1, non_deprecated_names assert deprecated_names # Do *not* use an explicit list of deprecated names explicit_deprecated_aliases_index = 0 else: # Keysym is deprecated assert ref.deprecated, ref ref_name = "" ref_index = ( "DEPRECATED_KEYSYM" if value < XKB_KEYSYM_UNICODE_MIN or value > XKB_KEYSYM_UNICODE_MAX else "UNICODE_KEYSYM" ) # Do *not* use an explicit list of deprecated names explicit_deprecated_aliases_index = 0 if non_deprecated_aliases := tuple(k for k in non_deprecated_names if k is not ref): non_deprecated = ( "Non deprecated aliases: " + ", ".join(k.name for k in non_deprecated_aliases) + ". " ) else: non_deprecated = "" deprecated = ", ".join(k.name for k in deprecated_names) comment = f"{ref_name}{non_deprecated}Deprecated: {deprecated}" return ( f" {{ 0x{value:0>8x}, {ref_index: <17}, {explicit_deprecated_aliases_index}, {len(deprecated_names_indices)} }}, /* {comment} */", deprecated_names_indices, ) def generate_deprecated_keysyms( all_keysyms: Keysyms, entry_offsets: dict[str, int] ) -> Generator[tuple[int, ...], None, None]: explicit_deprecated_aliases_index = 0 for value, keysyms in sorted(all_keysyms.by_value.items(), key=lambda e: e[0]): assert keysyms c_entry, explicit_deprecated_aliases = make_deprecated_entry( value, keysyms, entry_offsets, explicit_deprecated_aliases_index ) if c_entry is not None: print(c_entry) if explicit_deprecated_aliases: yield explicit_deprecated_aliases explicit_deprecated_aliases_index += len(explicit_deprecated_aliases) def generate_mixed_aliases(aliases: Iterable[Iterable[int]]): for xs in aliases: for x in xs: print(f" {x},") print(f"#define UNICODE_KEYSYM 0x{UNICODE_KEYSYM:x}") print(f"#define DEPRECATED_KEYSYM 0x{DEPRECATED_KEYSYM:x}") # NOTE: Alternative implementation, useful the day the indices do not fit uint16_t. # print(f""" # struct deprecated_keysym {{ # xkb_keysym_t keysym; # union {{ # uint32_t offset; # struct {{ # uint32_t offset:{MAX_OFFSET_LOG2}; # /* Explicit deprecated aliases start index & count */ # uint8_t explicit_index:{MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2}; # uint8_t explicit_count:{MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2}; # }} details; # }}; # }}; # """) print(""" struct deprecated_keysym { xkb_keysym_t keysym; uint16_t offset; /* Explicit deprecated aliases start index & count */ uint8_t explicit_index; uint8_t explicit_count; }; """) print("static const struct deprecated_keysym deprecated_keysyms[] = {") explicit_deprecated_aliases = tuple( generate_deprecated_keysyms(all_keysyms, entry_offsets) ) print("};\n") print("static const uint32_t explicit_deprecated_aliases[] = {") generate_mixed_aliases(explicit_deprecated_aliases) print("};") print(""" static_assert(ARRAY_SIZE(explicit_deprecated_aliases) < UINT8_MAX, "Cannot encode index and count in deprecated_keysym::explicit_*");\ """) print(f"max name offset: {max(entry_offsets.values())}", file=sys.stderr) # Check that the keywords of our XKB parser that clash with keysyms are handled properly def parse_gperf_keywords(path: Path) -> Iterator[str]: with path.open("rt", encoding="utf-8") as fd: in_keyword_section = False for line in fd: if line.startswith(r"%%"): # This is a boundary of the keywords section if in_keyword_section: break in_keyword_section = True elif in_keyword_section: # Parse the keywords keyword, *_ = line.split(",") yield keyword.strip().casefold() # Skip any line until we reach the keywords else: raise ValueError("Parse error: keywords section boundary not found") SUPPORTED_KEYWORDS_CLASHES = {"section"} UNSUPPORTED_KEYWORDS_CLASHES = frozenset(parse_gperf_keywords(args.gperf)).difference( SUPPORTED_KEYWORDS_CLASHES ) expected_clashes: set[str] = set() errors = 0 for entry in entries: if entry.name.casefold() in UNSUPPORTED_KEYWORDS_CLASHES: print( f"ERROR: keysym “{entry.name}” (0x{entry.value:0>4x}) clashes with keywords", "and cannot be parsed properly.", file=sys.stderr, ) errors += 1 elif (lower := entry.name.lower()) in SUPPORTED_KEYWORDS_CLASHES: if not entry.name.islower(): # Keywords’s atoms are registered in *lower* case, so the keysym will be # replaced by the keysym with the corresponding name, but they may not match. entry2: Keysym = Keysym( name="NoSymbol", value=0, char=None, char_aliases=[], char_semantics=Semantics.Default, deprecation=None, _canonical=None, _preferred=None, aliases=[], comment="", ) if any( e.name == lower for e in all_keysyms.by_value[entry.value] if e.name != entry.value ): # There is a keysym in lower case that is an alias print( f"WARNING: keysym “{entry.name}”", f"will be parsed as “{lower}” (expected)", file=sys.stderr, ) else: # Lookup the keysym mismatch for e in entries: if e.name == lower: entry2 = e break print( f"ERROR: keysym “{entry.name}” (0x{entry.value:0>4x})", r"clashes with keywords and will be replaced by", f"“{entry2.name}” (0x{entry2.value:0>4x}).", file=sys.stderr, ) errors += 1 else: print( f"WARNING: keysym “{entry.name}” clashing with keywords (expected)", file=sys.stderr, ) expected_clashes.add(entry.name) if diff := SUPPORTED_KEYWORDS_CLASHES.difference(expected_clashes): print(f"ERROR: Unexpected missing clashing keysyms: {diff}", file=sys.stderr) errors += 1 if errors: print( f" {errors} ERRORS ".center(80, "-"), "Please update the parser file `parser.y` to handle keysyms causing clashes.", "The relevant entries are:", "- Keysym", "- Element (for modmap, parsed via: Expr -> Term -> Lhs -> FieldSpec -> Element)", file=sys.stderr, sep="\n", ) exit(1)