update-keysyms-docstring.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. #!/usr/bin/env python3
  2. # Copyright © 2026 Pierre Le Marre <dev@wismill.eu>
  3. # SPDX-License-Identifier: MIT
  4. """
  5. Transform keysyms comments into docstring
  6. """
  7. import argparse
  8. import html
  9. import itertools
  10. import sys
  11. from pathlib import Path
  12. import tomllib
  13. # Root of the project
  14. SCRIPT = Path(__file__)
  15. sys.path.append(str(SCRIPT.parent))
  16. ROOT = SCRIPT.parent.parent
  17. DEFAULT_HEADER = ROOT / "include/xkbcommon/xkbcommon-keysyms.h"
  18. DEFAULT_AGE = ROOT / "data/keysyms/age.toml"
  19. from keysyms import ( # noqa: E402
  20. DeprecationReason,
  21. Keysym,
  22. KeysymCategory,
  23. Keysyms,
  24. Semantics,
  25. )
  26. # Parse commands
  27. parser = argparse.ArgumentParser(
  28. description="Transform keysyms comments into docstring"
  29. )
  30. parser.add_argument(
  31. "c_header",
  32. type=Path,
  33. default=DEFAULT_HEADER,
  34. help="Path to the libxkbcommon keysym header",
  35. )
  36. parser.add_argument(
  37. "--age",
  38. type=Path,
  39. default=DEFAULT_AGE,
  40. help="Path to the TOML file with keysyms age",
  41. )
  42. args = parser.parse_args()
  43. def semantics(s: Semantics) -> str:
  44. match s:
  45. case Semantics.Default:
  46. return ""
  47. case Semantics.ComputerNumpad:
  48. return "computer numpad"
  49. case Semantics.OtherKeypad:
  50. return "phone, remote controls and other keypads"
  51. case _:
  52. raise ValueError(s)
  53. def escape(raw: str):
  54. return (
  55. html.escape(raw, quote=False)
  56. .replace("\\", "\\\\")
  57. .replace("`", "\\`")
  58. .replace("@", "\\@")
  59. )
  60. def serialize(keysym: Keysym, age: str) -> str:
  61. prefix = "\n * "
  62. comment = f"{prefix}Keysym **{keysym.name}**"
  63. properties = f"{prefix}<dl>{prefix}<dt>Value</dt><dd>`0x{keysym.value:04x}`</dd>"
  64. deprecated = ""
  65. category_ref = keysym.category.casefold().replace(" ", "-") + "-keysyms"
  66. properties += (
  67. f"{prefix}<dt>Category</dt><dd>[{keysym.category}](@ref {category_ref})</dd>"
  68. )
  69. properties += f"{prefix}<dt>Preferred name</dt><dd>"
  70. if keysym.preferred is keysym:
  71. properties += "✅"
  72. elif keysym.deprecated:
  73. properties += "⚠️"
  74. else:
  75. properties += "ℹ️"
  76. properties += f" [{keysym.preferred.name}](@ref {keysym.preferred.macro}) ("
  77. if keysym.preferred is keysym:
  78. properties += "current name"
  79. elif keysym.deprecated:
  80. properties += "replacement"
  81. else:
  82. properties += "alternative"
  83. properties += ")</dd>"
  84. aliases: list[Keysym] = sorted(
  85. (
  86. k
  87. for k in itertools.chain((keysym.canonical,), keysym.canonical.aliases)
  88. if k is not keysym and k is not keysym.preferred
  89. ),
  90. key=lambda k: k.name,
  91. )
  92. if aliases:
  93. if keysym is keysym.preferred:
  94. properties += f"{prefix}<dt>Aliases</dt>"
  95. else:
  96. properties += f"{prefix}<dt>Other aliases</dt>"
  97. for alias in aliases:
  98. properties += "<dd>"
  99. properties += "🚫" if alias.deprecated else "ℹ️"
  100. properties += f" [{alias.name}](@ref {alias.macro})"
  101. if alias.deprecated:
  102. properties += " (deprecated)"
  103. properties += "</dd>"
  104. if (char := keysym.canonical.char) is not None:
  105. comment += prefix
  106. if keysym.canonical.deprecation is DeprecationReason.UNICODE_MISMATCH:
  107. deprecated += (
  108. f"{prefix}@deprecated Unclear correspondance in Unicode; closest "
  109. f"is: {char.markdown_cp}"
  110. )
  111. if c := char.some_char(printable=True):
  112. deprecated += f" “{escape(c)}”"
  113. approximation = "**⚠️ approximation:** "
  114. else:
  115. assert keysym.canonical.deprecation is None
  116. approximation = ""
  117. properties += f"{prefix}<dt>Unicode code point</dt><dd>{approximation}{char.markdown_cp}</dd>"
  118. if c := char.some_char(printable=True):
  119. properties += (
  120. f"{prefix}<dt>Character</dt><dd>{approximation}{escape(c)}</dd>"
  121. )
  122. if s := semantics(keysym.canonical.char_semantics):
  123. properties += f"{prefix}<dt>Special semantics</dt><dd>{s}</dd>"
  124. if keysym.canonical.char_aliases or keysym.category is KeysymCategory.Legacy:
  125. properties += f"{prefix}<dt>Keysyms with the same character</dt>"
  126. if keysym.category is KeysymCategory.Legacy:
  127. properties += (
  128. f"<dd>`U{char.cp:04X}`: [Unicode keysym](@ref unicode-keysyms)</dd>"
  129. )
  130. for char_alias in keysym.canonical.char_aliases:
  131. properties += f"<dd>[{char_alias.name}](@ref {char_alias.macro})"
  132. if s := semantics(char_alias.char_semantics):
  133. properties += f": {s}"
  134. properties += "</dd>"
  135. # TODO: Unicode keysym range
  136. elif keysym.deprecated:
  137. comment += prefix
  138. deprecated = f"{prefix}@deprecated "
  139. match keysym.deprecation:
  140. case DeprecationReason.TYPO:
  141. deprecated += "*Typo*"
  142. case DeprecationReason.IMPLICIT_ALIAS | DeprecationReason.LEGACY_ALIAS:
  143. deprecated += "*Legacy alias*"
  144. case DeprecationReason.UNKNOWN:
  145. deprecated += "*Legacy keysym*"
  146. case _:
  147. raise ValueError(keysym)
  148. if keysym.deprecated_keysym:
  149. if keysym.aliases:
  150. deprecated += (
  151. f"{prefix}@deprecated All the names of this keysym are deprecated"
  152. )
  153. elif keysym.preferred is not keysym:
  154. deprecated += f". Use `::{keysym.preferred.macro}` instead."
  155. else:
  156. raise ValueError(keysym)
  157. elif keysym.comment:
  158. comment += f": {keysym.comment.strip()}{prefix}"
  159. else:
  160. comment += prefix
  161. pass # TODO
  162. comment += deprecated
  163. if deprecated:
  164. comment += prefix
  165. properties += f"{prefix}</dl>"
  166. comment += properties
  167. if age:
  168. comment += f"{prefix}@since {age}"
  169. comment += f"{prefix}@addindex {keysym.name}"
  170. value = keysym.pretty_value
  171. return f"/**{comment}\n */\n#define {keysym.macro}\t{value}"
  172. if __name__ == "__main__":
  173. with args.age.open("rb") as f:
  174. ages = {
  175. name: entry["version"]
  176. for entry in tomllib.load(f).values()
  177. for name in entry["names"]
  178. }
  179. print("""\
  180. /**
  181. * @defgroup predefined-keysyms Predefined keysyms
  182. * List of *predefined* [keysyms](@ref xkb_keysym_t) names
  183. *
  184. * @ingroup keysyms
  185. * @{
  186. */
  187. """)
  188. for x in Keysyms.parse_iter_file(args.c_header):
  189. if isinstance(x, Keysym):
  190. print(serialize(x, ages.get(x.name, "")))
  191. else:
  192. print(x, end="")
  193. print("/** @} */")