| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- #!/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 <stddef.h>
- #include <stdint.h>
- #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)
|