#!/usr/bin/env python3 # Copyright © 2026 Pierre Le Marre # SPDX-License-Identifier: MIT """ Transform keysyms comments into docstring """ import argparse import html import itertools import sys from pathlib import Path import tomllib # Root of the project SCRIPT = Path(__file__) sys.path.append(str(SCRIPT.parent)) ROOT = SCRIPT.parent.parent DEFAULT_HEADER = ROOT / "include/xkbcommon/xkbcommon-keysyms.h" DEFAULT_AGE = ROOT / "data/keysyms/age.toml" from keysyms import ( # noqa: E402 DeprecationReason, Keysym, KeysymCategory, Keysyms, Semantics, ) # Parse commands parser = argparse.ArgumentParser( description="Transform keysyms comments into docstring" ) parser.add_argument( "c_header", type=Path, default=DEFAULT_HEADER, help="Path to the libxkbcommon keysym header", ) parser.add_argument( "--age", type=Path, default=DEFAULT_AGE, help="Path to the TOML file with keysyms age", ) args = parser.parse_args() def semantics(s: Semantics) -> str: match s: case Semantics.Default: return "" case Semantics.ComputerNumpad: return "computer numpad" case Semantics.OtherKeypad: return "phone, remote controls and other keypads" case _: raise ValueError(s) def escape(raw: str): return ( html.escape(raw, quote=False) .replace("\\", "\\\\") .replace("`", "\\`") .replace("@", "\\@") ) def serialize(keysym: Keysym, age: str) -> str: prefix = "\n * " comment = f"{prefix}Keysym **{keysym.name}**" properties = f"{prefix}
{prefix}
Value
`0x{keysym.value:04x}`
" deprecated = "" category_ref = keysym.category.casefold().replace(" ", "-") + "-keysyms" properties += ( f"{prefix}
Category
[{keysym.category}](@ref {category_ref})
" ) properties += f"{prefix}
Preferred name
" if keysym.preferred is keysym: properties += "✅" elif keysym.deprecated: properties += "⚠️" else: properties += "ℹ️" properties += f" [{keysym.preferred.name}](@ref {keysym.preferred.macro}) (" if keysym.preferred is keysym: properties += "current name" elif keysym.deprecated: properties += "replacement" else: properties += "alternative" properties += ")
" aliases: list[Keysym] = sorted( ( k for k in itertools.chain((keysym.canonical,), keysym.canonical.aliases) if k is not keysym and k is not keysym.preferred ), key=lambda k: k.name, ) if aliases: if keysym is keysym.preferred: properties += f"{prefix}
Aliases
" else: properties += f"{prefix}
Other aliases
" for alias in aliases: properties += "
" properties += "🚫" if alias.deprecated else "ℹ️" properties += f" [{alias.name}](@ref {alias.macro})" if alias.deprecated: properties += " (deprecated)" properties += "
" if (char := keysym.canonical.char) is not None: comment += prefix if keysym.canonical.deprecation is DeprecationReason.UNICODE_MISMATCH: deprecated += ( f"{prefix}@deprecated Unclear correspondance in Unicode; closest " f"is: {char.markdown_cp}" ) if c := char.some_char(printable=True): deprecated += f" “{escape(c)}”" approximation = "**⚠️ approximation:** " else: assert keysym.canonical.deprecation is None approximation = "" properties += f"{prefix}
Unicode code point
{approximation}{char.markdown_cp}
" if c := char.some_char(printable=True): properties += ( f"{prefix}
Character
{approximation}{escape(c)}
" ) if s := semantics(keysym.canonical.char_semantics): properties += f"{prefix}
Special semantics
{s}
" if keysym.canonical.char_aliases or keysym.category is KeysymCategory.Legacy: properties += f"{prefix}
Keysyms with the same character
" if keysym.category is KeysymCategory.Legacy: properties += ( f"
`U{char.cp:04X}`: [Unicode keysym](@ref unicode-keysyms)
" ) for char_alias in keysym.canonical.char_aliases: properties += f"
[{char_alias.name}](@ref {char_alias.macro})" if s := semantics(char_alias.char_semantics): properties += f": {s}" properties += "
" # TODO: Unicode keysym range elif keysym.deprecated: comment += prefix deprecated = f"{prefix}@deprecated " match keysym.deprecation: case DeprecationReason.TYPO: deprecated += "*Typo*" case DeprecationReason.IMPLICIT_ALIAS | DeprecationReason.LEGACY_ALIAS: deprecated += "*Legacy alias*" case DeprecationReason.UNKNOWN: deprecated += "*Legacy keysym*" case _: raise ValueError(keysym) if keysym.deprecated_keysym: if keysym.aliases: deprecated += ( f"{prefix}@deprecated All the names of this keysym are deprecated" ) elif keysym.preferred is not keysym: deprecated += f". Use `::{keysym.preferred.macro}` instead." else: raise ValueError(keysym) elif keysym.comment: comment += f": {keysym.comment.strip()}{prefix}" else: comment += prefix pass # TODO comment += deprecated if deprecated: comment += prefix properties += f"{prefix}
" comment += properties if age: comment += f"{prefix}@since {age}" comment += f"{prefix}@addindex {keysym.name}" value = keysym.pretty_value return f"/**{comment}\n */\n#define {keysym.macro}\t{value}" if __name__ == "__main__": with args.age.open("rb") as f: ages = { name: entry["version"] for entry in tomllib.load(f).values() for name in entry["names"] } print("""\ /** * @defgroup predefined-keysyms Predefined keysyms * List of *predefined* [keysyms](@ref xkb_keysym_t) names * * @ingroup keysyms * @{ */ """) for x in Keysyms.parse_iter_file(args.c_header): if isinstance(x, Keysym): print(serialize(x, ages.get(x.name, ""))) else: print(x, end="") print("/** @} */")