introspection-query.py.in 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  1. #!/usr/bin/env python3
  2. # Copyright © 2025 Pierre Le Marre <dev@wismill.eu>
  3. # SPDX-License-Identifier: MIT
  4. from __future__ import annotations
  5. import argparse
  6. from collections import defaultdict
  7. from collections.abc import Callable, Iterable, Iterator
  8. import dataclasses
  9. from dataclasses import dataclass
  10. from enum import StrEnum, auto, unique
  11. from functools import partial
  12. import itertools
  13. import multiprocessing
  14. import os
  15. from pathlib import Path
  16. import re
  17. import subprocess
  18. import sys
  19. import textwrap
  20. from typing import Any, ClassVar, Protocol, Self, TextIO, TypeVar, cast
  21. import rdflib
  22. import yaml
  23. # Meson needs to fill this in so we can call the tool in the buildir.
  24. EXTRA_PATH = "@MESON_BUILD_ROOT@"
  25. os.environ["PATH"] = ":".join(filter(bool, (EXTRA_PATH, os.getenv("PATH"))))
  26. @unique
  27. class Component(StrEnum):
  28. Keycodes = auto()
  29. Compatibility = auto()
  30. Geometry = auto()
  31. Symbols = auto()
  32. Types = auto()
  33. @classmethod
  34. def parse(cls, raw: str) -> Self:
  35. for c in cls:
  36. if c.value == raw:
  37. return c
  38. raise ValueError(raw)
  39. @property
  40. def dir(self) -> Path:
  41. match self:
  42. case self.Compatibility:
  43. return Path("compat")
  44. case _:
  45. return Path(self)
  46. @dataclass
  47. class Model:
  48. name: str
  49. @dataclass
  50. class Layout:
  51. name: str
  52. variants: set[str]
  53. PATTERN: ClassVar[re.Pattern[str]] = re.compile(
  54. r"^(?P<file>[^\(]+)(?:\((?P<section>[^\)]+)\))$"
  55. )
  56. @classmethod
  57. def parse(cls, raw: str) -> Self:
  58. if m := cls.PATTERN.match(raw):
  59. return cls(name=m.group("file"), variants={m.group("section")})
  60. else:
  61. return cls(name=raw, variants=set())
  62. @dataclass
  63. class Option:
  64. name: str
  65. @dataclass
  66. class RMLVO:
  67. rules: str
  68. model: str
  69. layout: str
  70. variant: str
  71. option: str
  72. def __iter__(self) -> Iterator[tuple[str, str]]:
  73. yield from dataclasses.asdict(self).items()
  74. @dataclass(unsafe_hash=True)
  75. class XkbFileRef:
  76. file: str
  77. section: str = ""
  78. path: Path | None = None
  79. PATTERN: ClassVar[re.Pattern[str]] = re.compile(
  80. r"^(?P<file>[^\(]+)(?:\((?P<section>[^\)]+)\))$"
  81. )
  82. @classmethod
  83. def parse(cls, raw: str) -> Self:
  84. if m := cls.PATTERN.match(raw):
  85. return cls(file=m.group("file"), section=m.group("section"))
  86. else:
  87. return cls(file=raw)
  88. def resolve_path(self, xkb_roots: Iterable[Path], component: Component) -> bool:
  89. args = ["introspection"] + list(
  90. itertools.chain.from_iterable(("--include", str(p)) for p in xkb_roots)
  91. )
  92. args += ["--resolve", "--type", str(component.dir)]
  93. if self.section:
  94. args += ["--section", self.section]
  95. args.append(self.file)
  96. try:
  97. completed = subprocess.run(args, check=True, capture_output=True)
  98. raw = yaml.safe_load(completed.stdout)
  99. self.path = raw["path"]
  100. self.section = raw["section"]
  101. return True
  102. except subprocess.CalledProcessError:
  103. print(f"ERROR cannot resolve: {self}", file=sys.stderr)
  104. return False
  105. @property
  106. def uri(self) -> str:
  107. if self.path is not None:
  108. return f"file:{self.path}#section={self.section}"
  109. else:
  110. # FIXME
  111. return f"file:{self.file}#section={self.section}"
  112. @dataclass
  113. class KcCGSTAcc:
  114. keycodes: set[XkbFileRef]
  115. compat: set[XkbFileRef]
  116. geometry: set[XkbFileRef]
  117. symbols: set[XkbFileRef]
  118. types: set[XkbFileRef]
  119. @classmethod
  120. def parse_component(cls, raw: str) -> Iterator[XkbFileRef]:
  121. start = 0
  122. for k in range(0, len(raw)):
  123. if raw[k] in ("+", "|", "^"):
  124. current = raw[start:k]
  125. start = k + 1
  126. if not current:
  127. continue
  128. raw_ref, *_ = current.split(":")
  129. yield XkbFileRef.parse(raw_ref)
  130. current = raw[start:]
  131. if current:
  132. raw_ref, *_ = current.split(":")
  133. yield XkbFileRef.parse(raw_ref)
  134. @classmethod
  135. def parse(cls, raw: dict[str, str]) -> Self:
  136. return cls(
  137. keycodes=set(cls.parse_component(raw["keycodes"])),
  138. compat=set(cls.parse_component(raw["compat"])),
  139. geometry=set(cls.parse_component(raw.get("geometry", ""))),
  140. symbols=set(cls.parse_component(raw["symbols"])),
  141. types=set(cls.parse_component(raw["types"])),
  142. )
  143. def __iter__(self) -> Iterator[tuple[Component, XkbFileRef]]:
  144. yield from ((Component.Keycodes, ref) for ref in self.keycodes)
  145. yield from ((Component.Compatibility, ref) for ref in self.compat)
  146. yield from ((Component.Geometry, ref) for ref in self.geometry)
  147. yield from ((Component.Symbols, ref) for ref in self.symbols)
  148. yield from ((Component.Types, ref) for ref in self.types)
  149. @classmethod
  150. def empty(cls) -> Self:
  151. return cls(set(), set(), set(), set(), set())
  152. def merge(self, other: Self):
  153. self.keycodes.update(other.keycodes)
  154. self.compat.update(other.compat)
  155. self.geometry.update(other.geometry)
  156. self.symbols.update(other.symbols)
  157. self.types.update(other.types)
  158. @classmethod
  159. def _resolve_paths(
  160. cls, xkb_roots: Iterable[Path], component: Component, refs: Iterable[XkbFileRef]
  161. ):
  162. for ref in refs:
  163. ref.resolve_path(xkb_roots, component)
  164. def resolve_paths(self, *xkb_roots: Path):
  165. self._resolve_paths(
  166. xkb_roots=xkb_roots, component=Component.Keycodes, refs=self.keycodes
  167. )
  168. self._resolve_paths(
  169. xkb_roots=xkb_roots, component=Component.Compatibility, refs=self.compat
  170. )
  171. self._resolve_paths(
  172. xkb_roots=xkb_roots, component=Component.Geometry, refs=self.geometry
  173. )
  174. self._resolve_paths(
  175. xkb_roots=xkb_roots, component=Component.Symbols, refs=self.symbols
  176. )
  177. self._resolve_paths(
  178. xkb_roots=xkb_roots, component=Component.Types, refs=self.types
  179. )
  180. @dataclass
  181. class Registry:
  182. rules: str
  183. models: tuple[Model, ...]
  184. layouts: tuple[Layout, ...]
  185. options: tuple[Option, ...]
  186. DEFAULT_RULES: ClassVar[str] = "@DEFAULT_XKB_RULES@"
  187. DEFAULT_MODEL: ClassVar[str] = "@DEFAULT_XKB_MODEL@"
  188. DEFAULT_LAYOUT: ClassVar[str] = "@DEFAULT_XKB_LAYOUT@"
  189. DEFAULT_VARIANT: ClassVar[str] = "@DEFAULT_XKB_VARIANT@"
  190. DEFAULT_OPTIONS: ClassVar[str] = "@DEFAULT_XKB_OPTIONS@"
  191. @classmethod
  192. def parse(cls, rules: str, *include: Path) -> Self:
  193. args = (
  194. "xkbcli-list",
  195. "--ruleset",
  196. rules,
  197. "--load-exotic",
  198. "--skip-default-paths",
  199. ) + tuple(map(str, include))
  200. try:
  201. completed = subprocess.run(args, check=True, capture_output=True)
  202. except subprocess.CalledProcessError as err:
  203. raise ValueError(err.stderr)
  204. raw: dict[str, Any] = yaml.safe_load(completed.stdout)
  205. return cls(
  206. rules=rules,
  207. models=tuple(cls.parse_models(raw)),
  208. layouts=tuple(cls.parse_layouts(raw)),
  209. options=tuple(cls.parse_options(raw)),
  210. )
  211. @classmethod
  212. def parse_models(cls, raw: dict[str, Iterable[dict[str, str]]]) -> Iterator[Model]:
  213. for model in raw["models"]:
  214. yield Model(model["name"])
  215. @classmethod
  216. def parse_layouts(
  217. cls, raw: dict[str, Iterable[dict[str, str]]]
  218. ) -> Iterator[Layout]:
  219. layouts: dict[str, set[str]] = defaultdict(set)
  220. for layout in raw["layouts"]:
  221. layouts[layout["layout"]].add(layout.get("variant", ""))
  222. yield from (
  223. Layout(name=layout, variants=variants)
  224. for layout, variants in layouts.items()
  225. )
  226. @classmethod
  227. def parse_options(
  228. cls, raw: dict[str, Iterable[dict[str, Any]]]
  229. ) -> Iterator[Option]:
  230. for option_group in raw["option_groups"]:
  231. for option in option_group["options"]:
  232. yield Option(option["name"])
  233. def mlvo_iterator(
  234. self,
  235. models: Iterable[Model] | None = None,
  236. layouts: Iterable[Layout] | None = None,
  237. options: Iterable[Option] | None = None,
  238. ) -> tuple[int, Callable[[], Iterator[RMLVO]]]:
  239. models = self.models if models is None else models
  240. layouts = self.layouts if layouts is None else layouts
  241. options = self.options if options is None else options
  242. # In order to avoid combinatorial explosion, we limit to
  243. # model/layout/variant and layout/variant/options combinations.
  244. count1 = len(models) * sum(len(l.variants) for l in layouts)
  245. count2 = sum(len(l.variants) for l in layouts) * len(options)
  246. count = count1 + count2
  247. def iterate():
  248. for m in models:
  249. for l in layouts:
  250. for v in l.variants:
  251. yield RMLVO(
  252. rules=self.rules,
  253. model=m.name,
  254. layout=l.name,
  255. variant=v,
  256. option="",
  257. )
  258. for opt in options:
  259. for m in (Model(self.DEFAULT_MODEL),):
  260. for l in layouts:
  261. for v in l.variants:
  262. yield RMLVO(
  263. rules=self.rules,
  264. model=m.name,
  265. layout=l.name,
  266. variant=v,
  267. option=opt.name,
  268. )
  269. return count, iterate
  270. @classmethod
  271. def _get_kccgst(
  272. cls, xkb_roots: tuple[Path, ...], rmlvo: RMLVO
  273. ) -> tuple[bool, KcCGSTAcc]:
  274. args = ["xkbcli-compile-keymap", "--kccgst-yaml"]
  275. args += list(
  276. itertools.chain.from_iterable(("--include", str(p)) for p in xkb_roots)
  277. )
  278. args += list(itertools.chain.from_iterable((f"--{c}", v) for c, v in rmlvo))
  279. try:
  280. completed = subprocess.run(args, check=True, capture_output=True)
  281. raw = yaml.safe_load(completed.stdout)
  282. return True, KcCGSTAcc.parse(raw)
  283. except subprocess.CalledProcessError:
  284. return False, KcCGSTAcc.empty()
  285. def get_kccgst(
  286. self,
  287. xkb_roots: Iterable[Path],
  288. combos: Iterable[RMLVO],
  289. combos_count: int,
  290. njobs: int,
  291. chunksize: int,
  292. progress_bar: ProgressBar[Iterable[tuple[bool, KcCGSTAcc]]],
  293. ) -> KcCGSTAcc:
  294. # failed = False
  295. acc = KcCGSTAcc.empty()
  296. roots = tuple(xkb_roots)
  297. with multiprocessing.Pool(njobs) as p:
  298. f = partial(self._get_kccgst, roots)
  299. results = p.imap_unordered(f, combos, chunksize=chunksize)
  300. for ok, result in progress_bar(
  301. results, total=combos_count, file=sys.stdout
  302. ):
  303. if not ok:
  304. # failed = True
  305. pass
  306. else:
  307. acc.merge(result)
  308. return acc
  309. T = TypeVar("T")
  310. # Needed because Callable does not handle keywords args
  311. class ProgressBar(Protocol[T]):
  312. def __call__(self, x: T, total: int, file: TextIO | None) -> T: ...
  313. # The function generating the progress bar (if any).
  314. def create_progress_bar(verbose: bool) -> ProgressBar[T]:
  315. def noop_progress_bar(x: T, total: int, file: TextIO | None = None) -> T:
  316. return x
  317. progress_bar: ProgressBar[T] = noop_progress_bar
  318. if not verbose and os.isatty(sys.stdout.fileno()):
  319. try:
  320. from tqdm import tqdm
  321. progress_bar = cast(ProgressBar[T], tqdm)
  322. except ImportError:
  323. pass
  324. return progress_bar
  325. @unique
  326. class OutputFormat(StrEnum):
  327. Dot = auto()
  328. Rdf = auto()
  329. # Svg = auto()
  330. Yaml = auto()
  331. # RDF namespaces
  332. namespaces = {"xkb": "xkb:", "flags": "xkb:flags/"}
  333. def parse_file(path: Path) -> rdflib.Graph:
  334. g = rdflib.Graph()
  335. return g.parse(path)
  336. def run_dependencies(args: argparse.Namespace):
  337. g = parse_file(args.path)
  338. property_path = "(xkb:includes+/(rdf:first|rdf:rest)+/rdf:first)"
  339. query_keymap = "SELECT (true AS ?ok) WHERE { [] rdf:type xkb:keymap . }"
  340. query_root = """
  341. SELECT ?path ?section
  342. WHERE {
  343. [] rdf:type xkb:Introspection;
  344. xkb:path ?path ;
  345. xkb:section ?section .
  346. }
  347. """
  348. query_default = """
  349. SELECT ?path ?section ?index ?default
  350. WHERE {
  351. [] rdf:type xkb:Introspection;
  352. xkb:path ?path .
  353. ?node
  354. xkb:path ?path ;
  355. xkb:section ?section ;
  356. xkb:section-index ?index ;
  357. OPTIONAL {
  358. ?node xkb:flag flags:default .
  359. BIND (true as ?default)
  360. }
  361. }
  362. ORDER BY ASC(?index)
  363. """
  364. if args.file:
  365. # Look for specific file
  366. ref = XkbFileRef(file=args.file, section=args.section or "")
  367. if args.file.is_absolute():
  368. ref.path = args.file.resolve()
  369. elif args.type:
  370. if not ref.resolve_path(xkb_roots=args.include or (), component=args.type):
  371. raise ValueError(f"Cannot resolve: {ref}")
  372. else:
  373. raise ValueError(f"Missing file type to resolve {ref}")
  374. node = f"<{ref.uri}>"
  375. elif r := g.query(query_keymap, initNs=namespaces):
  376. # Look for keymap sections
  377. node = "[ rdf:type xkb:keymap ] xkb:includes ?node . ?node"
  378. elif rs := g.query(query_root, initNs=namespaces):
  379. r0 = tuple(rs)[0]
  380. if args.debug:
  381. print(r0, file=sys.stderr)
  382. path = Path(r0[0].toPython())
  383. # Try to use the section from the CLI or from the xkb:Inspection
  384. if (section := args.section) is None and not (
  385. section := tuple(rs)[0][1].toPython()
  386. ):
  387. # No section defined: look for the default map (implicit or explicit)
  388. if rs := g.query(query_default, initNs=namespaces):
  389. rs = tuple(rs)
  390. r0 = rs[0]
  391. path = Path(r0[0].toPython())
  392. for r in rs:
  393. if r[3].toPython():
  394. r0 = r
  395. break
  396. if args.debug:
  397. print(
  398. *map(
  399. lambda x: tuple(
  400. map(lambda f: None if f is None else f.toPython(), x)
  401. ),
  402. rs,
  403. ),
  404. sep="\n",
  405. file=sys.stderr,
  406. )
  407. print("Found:", r0, file=sys.stderr)
  408. section = r0[1]
  409. else:
  410. raise ValueError("Cannot determine map")
  411. ref = XkbFileRef(path, section, path)
  412. node = f"<{ref.uri}>"
  413. else:
  414. raise ValueError()
  415. type_ = (
  416. f"VALUES (?type) {{ (xkb:{args.type.value}) }}" if args.type is not None else ""
  417. )
  418. transitive = "+" if args.transitive else ""
  419. query = textwrap.dedent(f"""\
  420. SELECT DISTINCT ?type ?path ?section
  421. WHERE {{
  422. {type_}
  423. {node}
  424. rdf:type ?type ;
  425. {property_path}{transitive} [
  426. xkb:path ?path ;
  427. xkb:section ?section
  428. ] .
  429. }}
  430. """)
  431. if args.debug:
  432. print(query, file=sys.stderr)
  433. results: dict[str, list[dict[str, str]]] = defaultdict(list)
  434. for r in g.query(query, initNs=namespaces):
  435. data = {"path": r[1].toPython(), "section": r[2].toPython()}
  436. results[r[0].toPython().split(":")[1]].append(data)
  437. for r in results.values():
  438. r.sort(key=lambda x: x["path"])
  439. yaml.dump(dict(results), stream=sys.stdout)
  440. def run_use(args: argparse.Namespace):
  441. g = parse_file(args.path)
  442. # Apply inference
  443. xkb = rdflib.Namespace("xkb:")
  444. used_in = xkb["used-in-rules"]
  445. inc = xkb["includes"]
  446. ts = []
  447. def traverseList(node, g):
  448. for f in g.objects(node, rdflib.RDF.first):
  449. yield f
  450. for r in g.objects(node, rdflib.RDF.rest):
  451. yield from traverseList(r, g)
  452. def includes(node, g):
  453. for l in g.objects(node, inc):
  454. for x in g.transitiveClosure(traverseList, l):
  455. for y in g.transitiveClosure(traverseList, x):
  456. yield from g.objects(y, inc)
  457. for f, rules in g.subject_objects(used_in):
  458. for node in g.transitiveClosure(includes, f):
  459. ts.append((node, used_in, rules))
  460. for t in ts:
  461. g.add(t)
  462. type_ = (
  463. f"VALUES (?type) {{ (xkb:{args.type.value}) }}" if args.type is not None else ""
  464. )
  465. rules = f'VALUES (?rules) {{ ("{args.rules}") }}' if args.rules is not None else ""
  466. if args.file:
  467. # Check for a specific file
  468. if args.type is None:
  469. raise ValueError("Missing mandatory component type")
  470. ref = XkbFileRef(file=args.file, section=args.section)
  471. ref.resolve_path(xkb_roots=args.include, component=args.type)
  472. values = "VALUES " + (
  473. f'(?path ?section) {{ ("{ref.path}" "{ref.section}") }}'
  474. if args.section
  475. else f'(?path) {{ ("{ref.path}") }}'
  476. )
  477. else:
  478. values = ""
  479. # property_path = "(xkb:includes/(rdf:first|rdf:rest)+/rdf:first/xkb:includes)"
  480. if args.unused:
  481. # filter = f"""\
  482. # FILTER NOT EXISTS {{ ?node xkb:used-in-rules ?rules . }}
  483. # FILTER NOT EXISTS {{
  484. # ?parent {property_path}+ ?node ;
  485. # rdf:type ?type ;
  486. # xkb:used-in-rules ?rules .
  487. # }}"""
  488. filter = "FILTER NOT EXISTS { ?node xkb:used-in-rules ?rules . }"
  489. else:
  490. # filter = f"""\
  491. # FILTER EXISTS {{
  492. # {{ ?node xkb:used-in-rules ?rules . }}
  493. # UNION
  494. # {{
  495. # ?parent {property_path}+ ?node ;
  496. # rdf:type ?type ;
  497. # xkb:used-in-rules ?rules .
  498. # }}
  499. # }}
  500. # """
  501. filter = "?node xkb:used-in-rules ?rules ."
  502. query = textwrap.dedent(f"""\
  503. SELECT DISTINCT ?rules ?type ?path ?section
  504. WHERE {{
  505. {type_}
  506. {rules}
  507. {values}
  508. ?node
  509. rdf:type ?type ;
  510. xkb:path ?path ;
  511. xkb:section ?section .
  512. {filter}
  513. FILTER NOT EXISTS {{ ?node rdf:type xkb:Introspection }}
  514. }}
  515. """)
  516. if args.debug:
  517. print(query, file=sys.stderr)
  518. result: dict[str, dict[str, str]] = defaultdict(list)
  519. for r in g.query(query, initNs=namespaces):
  520. data = {"path": r[2].toPython(), "section": r[3].toPython()}
  521. if not args.unused:
  522. data["rules"] = r[0].toPython()
  523. # if args.type is None:
  524. # data["type"] = r[1].toPython().split(":")[1]
  525. ty = r[1].toPython().split(":")[1]
  526. result[ty].append(data)
  527. for r in result.values():
  528. r.sort(key=lambda x: x["path"])
  529. yaml.dump(dict(result), stream=sys.stdout)
  530. def run_query(args: argparse.Namespace):
  531. g = parse_file(args.path)
  532. for r in g.query(args.query, initNs=namespaces):
  533. print(r)
  534. def run_pretty(args: argparse.Namespace):
  535. g = parse_file(args.path)
  536. print(g.serialize())
  537. def get_xkb_files(path: Path, components: Iterable[Component]) -> Iterator[Path]:
  538. for component in components:
  539. for dirpath, _dirnames, filenames in (path / component.dir).walk():
  540. # Discard files with suffixes, such as *.md files
  541. yield from (
  542. dirpath / f
  543. for f in map(Path, filenames)
  544. if not f.suffix and f.stem != "README"
  545. )
  546. def run_process_trees(args: argparse.Namespace):
  547. components = args.type if args.type else Component
  548. paths = tuple(
  549. itertools.chain.from_iterable(
  550. map(partial(get_xkb_files, components=components), args.path)
  551. )
  552. )
  553. tool_args: list[str] = ["introspection"]
  554. tool_args += list(
  555. itertools.chain.from_iterable(("--include", p) for p in args.path)
  556. )
  557. tool_args += args.extra
  558. tool_args += list(map(str, paths))
  559. try:
  560. completed = subprocess.run(
  561. tool_args,
  562. check=True,
  563. )
  564. return completed.returncode
  565. except subprocess.CalledProcessError as err:
  566. return err.returncode
  567. def run_process_rules(args: argparse.Namespace):
  568. registry = Registry.parse(args.rules, *args.path)
  569. count, iterator = registry.mlvo_iterator(
  570. models=args.model, layouts=args.layout, options=args.option
  571. )
  572. progress_bar: ProgressBar[Iterable[Any]] = create_progress_bar(False)
  573. acc = registry.get_kccgst(
  574. xkb_roots=args.path,
  575. combos=iterator(),
  576. combos_count=count,
  577. njobs=args.jobs,
  578. chunksize=args.chunksize,
  579. progress_bar=progress_bar,
  580. )
  581. acc.resolve_paths(*args.path)
  582. with args.output.open("wt", encoding="utf-8") as fd:
  583. if args.rdf:
  584. print("@prefix\txkb:\t<xkb:> .", file=fd)
  585. for _ty, ref in acc:
  586. print(f'<{ref.uri}>\txkb:used-in-rules\t"{args.rules}" .', file=fd)
  587. else:
  588. prev_ty = None
  589. for ty, ref in acc:
  590. if ty != prev_ty:
  591. print(f"{ty}:", file=fd)
  592. prev_ty = ty
  593. print(f'- file: "{ref.file}"', file=fd)
  594. print(f' section: "{ref.section}"', file=fd)
  595. if ref.path is not None:
  596. print(f' path: "{ref.path}"', file=fd)
  597. else:
  598. print(" path: null", file=fd)
  599. if __name__ == "__main__":
  600. parser = argparse.ArgumentParser(
  601. # FIXME: description
  602. description="Tool to process the RDF output of xkbcommon introspection"
  603. )
  604. parser.add_argument("--debug", action="store_true", help="Debug mode")
  605. subparsers = parser.add_subparsers()
  606. # Dependencies
  607. parser_deps = subparsers.add_parser(
  608. "dependencies", aliases=("deps",), help="Dependencies of a map"
  609. )
  610. parser_deps.add_argument(
  611. "path", type=argparse.FileType("rt", encoding="utf-8"), help="RDF Turtle file"
  612. )
  613. parser_deps.add_argument(
  614. "-i", "--include", type=Path, action="append", default=[], help="XKB root path"
  615. )
  616. parser_deps.add_argument("-f", "--file", type=Path, help="XKB file")
  617. parser_deps.add_argument("-s", "--section", type=str, help="Section in an XKB file")
  618. parser_deps.add_argument(
  619. "-t", "--type", type=Component.parse, help="Component type"
  620. )
  621. parser_deps.add_argument(
  622. "-T", "--transitive", action="store_true", help="Enable transitive dependencies"
  623. )
  624. parser_deps.set_defaults(run=run_dependencies)
  625. # Used/unused
  626. parser_use = subparsers.add_parser("use", help="List used/unused file & sections")
  627. parser_use.add_argument(
  628. "path", type=argparse.FileType("rt", encoding="utf-8"), help="RDF Turtle file"
  629. )
  630. parser_use.add_argument(
  631. "-U", "--unused", action="store_true", help="Search unused files"
  632. )
  633. parser_use.add_argument(
  634. "-i", "--include", type=Path, action="append", default=[], help="XKB root path"
  635. )
  636. parser_use.add_argument("-r", "--rules", type=str, help="XKB ruleset")
  637. parser_use.add_argument("-f", "--file", type=Path, help="XKB file")
  638. parser_use.add_argument("-s", "--section", type=str, help="Section in an XKB file")
  639. parser_use.add_argument("-t", "--type", type=Component.parse, help="Component type")
  640. parser_use.set_defaults(run=run_use)
  641. # Query
  642. parser_query = subparsers.add_parser("query", help="SPARQL query")
  643. parser_query.add_argument(
  644. "path", type=argparse.FileType("rt", encoding="utf-8"), help="RDF Turtle file"
  645. )
  646. parser_query.add_argument(
  647. "-q", "--query", type=str, help="SPARQL query", required=True
  648. )
  649. parser_query.set_defaults(run=run_query)
  650. # Prettyfier
  651. parser_pretty = subparsers.add_parser("pretty", help="Prettyfier")
  652. parser_pretty.add_argument(
  653. "path", type=argparse.FileType("rt", encoding="utf-8"), help="RDF Turtle file"
  654. )
  655. parser_pretty.set_defaults(run=run_pretty)
  656. # XKB tree analyzer
  657. parser_tree = subparsers.add_parser(
  658. "tree",
  659. help="Analyze XKB trees",
  660. description="Analyze XKB trees",
  661. epilog="Use `--` to pass extra arguments to the `introspection` tool",
  662. )
  663. parser_tree.add_argument(
  664. "path",
  665. nargs="*",
  666. type=Path,
  667. help="Path to an XKB tree",
  668. )
  669. parser_tree.add_argument(
  670. "-t",
  671. "--type",
  672. default=[],
  673. action="append",
  674. type=Component.parse,
  675. help="Component type",
  676. )
  677. parser_tree.set_defaults(run=run_process_trees)
  678. # XKB rules analyzer
  679. parser_rules = subparsers.add_parser(
  680. "rules",
  681. help="Analyze XKB rules",
  682. description="Analyze XKB rules",
  683. epilog="Use `--` to pass extra arguments to the `introspection` tool",
  684. )
  685. parser_rules.add_argument(
  686. "path",
  687. nargs="*",
  688. type=Path,
  689. help="Path to an XKB tree",
  690. )
  691. parser_rules.add_argument(
  692. "-j",
  693. "--jobs",
  694. type=int,
  695. default=4 * (os.cpu_count() or 1),
  696. help="number of processes to use",
  697. )
  698. parser_rules.add_argument("--chunksize", default=1, type=int)
  699. parser_rules.add_argument(
  700. "-r",
  701. "--rules",
  702. default="evdev",
  703. type=str,
  704. help="Ruleset",
  705. )
  706. parser_rules.add_argument(
  707. "-m",
  708. "--model",
  709. action="append",
  710. type=Model,
  711. help="Only specific models",
  712. )
  713. parser_rules.add_argument(
  714. "-l",
  715. "--layout",
  716. action="append",
  717. type=Layout.parse,
  718. help="Only specific layouts",
  719. )
  720. parser_rules.add_argument(
  721. "-o",
  722. "--option",
  723. action="append",
  724. type=Option,
  725. help="Only specific options",
  726. )
  727. parser_rules.add_argument(
  728. "-t",
  729. "--type",
  730. default=[],
  731. action="append",
  732. type=Component.parse,
  733. help="Component type",
  734. )
  735. parser_rules.add_argument(
  736. "--rdf",
  737. action="store_true",
  738. help="Output in RDF format",
  739. )
  740. parser_rules.add_argument("--output", type=Path, help="Output file", required=True)
  741. parser_rules.set_defaults(run=run_process_rules)
  742. # Misc
  743. if "--" in sys.argv:
  744. idx = sys.argv.index("--")
  745. argv = sys.argv[1:idx]
  746. extra = sys.argv[idx + 1 :]
  747. else:
  748. argv = sys.argv[1:]
  749. extra = []
  750. # Run
  751. args = parser.parse_args(argv)
  752. args.extra = extra
  753. # [HACK]
  754. if hasattr(args, "option") and args.option == [Option("-")]:
  755. args.option = ()
  756. exit(args.run(args))