Files
2026-07-13 12:20:32 +08:00

99 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Dump the accessibility tree of running applications.
Usage:
a11y-tree.py # list all accessible apps
a11y-tree.py <app-name> # dump tree for matching app (substring match)
a11y-tree.py --depth N # limit tree depth (default: 5)
"""
import gi, sys, argparse
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
STATE_NAMES = {}
for _attr in dir(Atspi.StateType):
_val = getattr(Atspi.StateType, _attr)
if isinstance(_val, int) and not _attr.startswith("_"):
STATE_NAMES[_val] = _attr.lower().replace("_", "-")
def dump_node(node, indent=0, max_depth=5):
if indent > max_depth:
return
try:
name = node.get_name() or ""
role = node.get_role_name()
states = []
state_set = node.get_state_set()
for s in range(Atspi.StateType.LAST_DEFINED):
if state_set.contains(s):
states.append(STATE_NAMES.get(s, str(s)))
text = ""
try:
ifaces = node.get_interfaces()
if "Text" in ifaces:
n_chars = node.get_character_count()
if n_chars > 0:
text = node.get_text(0, min(n_chars, 80))
if n_chars > 80:
text += "..."
except Exception:
pass
prefix = " " * indent
line = f"{prefix}[{role}]"
if name:
line += f' "{name}"'
if text and text != name:
line += f" text={text!r}"
if states:
line += f" ({', '.join(states)})"
print(line)
for i in range(node.get_child_count()):
child = node.get_child_at_index(i)
if child:
dump_node(child, indent + 1, max_depth)
except Exception as e:
print(f"{' ' * indent}[error: {e}]")
def main():
parser = argparse.ArgumentParser(description="Dump AT-SPI accessibility tree")
parser.add_argument("app", nargs="?", help="App name substring to match")
parser.add_argument("--depth", type=int, default=30, help="Max tree depth")
args = parser.parse_args()
Atspi.init()
desktop = Atspi.get_desktop(0)
n = desktop.get_child_count()
if n == 0:
print("No accessible apps found. Make sure GTK_MODULES=gail:atk-bridge is set.")
sys.exit(1)
if not args.app:
print(f"Accessible apps ({n}):")
for i in range(n):
child = desktop.get_child_at_index(i)
print(f" {child.get_name() or '(unnamed)'} [{child.get_role_name()}]")
return
for i in range(n):
child = desktop.get_child_at_index(i)
name = child.get_name() or ""
if args.app.lower() in name.lower():
print(f"=== {name} [{child.get_role_name()}] ===")
dump_node(child, max_depth=args.depth)
return
print(f"No app matching '{args.app}' found.")
print("Available:", ", ".join(
desktop.get_child_at_index(i).get_name() or "(unnamed)" for i in range(n)
))
if __name__ == "__main__":
main()