update-keysyms-derived-headers.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env python3
  2. """
  3. Generate headers related to keysyms bounds
  4. """
  5. import argparse
  6. import itertools
  7. import re
  8. import sys
  9. from collections import defaultdict
  10. from pathlib import Path
  11. from typing import Any, TypeAlias
  12. import jinja2
  13. KEYSYM_PATTERN = re.compile(
  14. r"^#define\s+XKB_KEY_(?P<name>\w+)\s+(?P<value>0x[0-9a-fA-F]+)\b"
  15. )
  16. MAX_AMBIGUOUS_NAMES = 3
  17. KeysymsBounds: TypeAlias = dict[str, int | str]
  18. KeysymsCaseFoldedNames: TypeAlias = dict[str, list[str]]
  19. def load_keysyms(path: Path) -> tuple[KeysymsBounds, KeysymsCaseFoldedNames]:
  20. # Load the keysyms header
  21. keysym_min = sys.maxsize
  22. keysym_max = 0
  23. min_unicode_keysym = 0x01000100
  24. max_unicode_keysym = 0x0110FFFF
  25. names: dict[int, set[str]] = defaultdict(set)
  26. canonical_names: dict[int, str] = {}
  27. casefolded_names: dict[str, list[str]] = defaultdict(list)
  28. max_unicode_name = "U10FFFF"
  29. max_keysym_name = "0x1fffffff" # XKB_KEYSYM_MAX
  30. with path.open("rt", encoding="utf-8") as fd:
  31. for line in fd:
  32. if m := KEYSYM_PATTERN.match(line):
  33. value = int(m.group("value"), 16)
  34. keysym_min = min(keysym_min, value)
  35. keysym_max = max(keysym_max, value)
  36. name = m.group("name")
  37. names[value].add(name)
  38. casefolded_names[name.casefold()].append(name)
  39. if value not in canonical_names:
  40. canonical_names[value] = name
  41. XKB_KEYSYM_LONGEST_CANONICAL_NAME = max(
  42. max(canonical_names.values(), key=len),
  43. max_unicode_name,
  44. max_keysym_name,
  45. key=len,
  46. )
  47. XKB_KEYSYM_LONGEST_NAME = max(
  48. max(itertools.chain.from_iterable(casefolded_names.values()), key=len),
  49. max_unicode_name,
  50. max_keysym_name,
  51. key=len,
  52. )
  53. XKB_KEYSYM_EXPLICIT_ALIASES_MAX = max(map(len, names.values()))
  54. # Keep only ambiguous case-insensitive names and sort them
  55. for name in tuple(casefolded_names.keys()):
  56. count = len(casefolded_names[name])
  57. if count < 2:
  58. del casefolded_names[name]
  59. elif count > MAX_AMBIGUOUS_NAMES:
  60. raise ValueError(
  61. f"""Expected max {MAX_AMBIGUOUS_NAMES} keysyms for "{name}", got: {count}"""
  62. )
  63. else:
  64. casefolded_names[name].sort()
  65. return (
  66. {
  67. "XKB_KEYSYM_MIN_ASSIGNED": min(keysym_min, min_unicode_keysym),
  68. "XKB_KEYSYM_MAX_ASSIGNED": max(keysym_max, max_unicode_keysym),
  69. "XKB_KEYSYM_MIN_EXPLICIT": keysym_min,
  70. "XKB_KEYSYM_MAX_EXPLICIT": keysym_max,
  71. "XKB_KEYSYM_COUNT_EXPLICIT": len(canonical_names),
  72. # Extra byte for terminating NULL
  73. "XKB_KEYSYM_NAME_MAX_SIZE": len(XKB_KEYSYM_LONGEST_CANONICAL_NAME) + 1,
  74. "XKB_KEYSYM_LONGEST_CANONICAL_NAME": XKB_KEYSYM_LONGEST_CANONICAL_NAME,
  75. "XKB_KEYSYM_LONGEST_NAME": XKB_KEYSYM_LONGEST_NAME,
  76. "XKB_KEYSYM_EXPLICIT_ALIASES_MAX": XKB_KEYSYM_EXPLICIT_ALIASES_MAX,
  77. },
  78. casefolded_names,
  79. )
  80. def generate(
  81. env: jinja2.Environment,
  82. data: dict[str, Any],
  83. root: Path,
  84. file: Path,
  85. ) -> None:
  86. """Generate a file from its Jinja2 template"""
  87. template_path = file.with_suffix(f"{file.suffix}.jinja")
  88. template = env.get_template(str(template_path))
  89. path = root / file
  90. with path.open("wt", encoding="utf-8") as fd:
  91. fd.writelines(template.generate(**data))
  92. # Root of the project
  93. SCRIPT = Path(__file__)
  94. ROOT = SCRIPT.parent.parent
  95. # Parse commands
  96. parser = argparse.ArgumentParser(
  97. description="Generate C header files related to keysyms bounds"
  98. )
  99. parser.add_argument(
  100. "--root",
  101. type=Path,
  102. default=ROOT,
  103. help="Path to the root of the project (default: %(default)s)",
  104. )
  105. args = parser.parse_args()
  106. # Configure Jinja
  107. template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8")
  108. jinja_env = jinja2.Environment(
  109. loader=template_loader,
  110. keep_trailing_newline=True,
  111. trim_blocks=True,
  112. lstrip_blocks=True,
  113. )
  114. jinja_env.filters["keysym"] = lambda ks: f"0x{ks:0>8x}"
  115. # Load keysyms
  116. keysyms_bounds, keysyms_ambiguous_case_insensitive_names = load_keysyms(
  117. args.root / "include/xkbcommon/xkbcommon-keysyms.h"
  118. )
  119. # Generate the files
  120. generate(
  121. jinja_env,
  122. dict(
  123. keysyms_bounds,
  124. script=SCRIPT.relative_to(ROOT),
  125. ),
  126. args.root,
  127. Path("src/keysym.h"),
  128. )
  129. generate(
  130. jinja_env,
  131. dict(
  132. keysyms_bounds,
  133. ambiguous_case_insensitive_names=keysyms_ambiguous_case_insensitive_names,
  134. MAX_AMBIGUOUS_NAMES=MAX_AMBIGUOUS_NAMES,
  135. script=SCRIPT.relative_to(ROOT),
  136. ),
  137. args.root,
  138. Path("test/keysym.h"),
  139. )