feat: add accessibility tree explorer script for development
Findings: Cursor uses bundle ID com.todesktop.230313mzl4w4u92. Electron web content exposed as AXStaticText values within AXWebArea, not AXButton titles. Detector must search AXStaticText elements. Made-with: Cursor
This commit is contained in:
92
scripts/dump_a11y_tree.py
Normal file
92
scripts/dump_a11y_tree.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""Dump the accessibility tree of the Cursor application.
|
||||||
|
|
||||||
|
Usage: python scripts/dump_a11y_tree.py [--depth N]
|
||||||
|
|
||||||
|
Requires Accessibility permissions for the running terminal.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ApplicationServices import (
|
||||||
|
AXUIElementCreateApplication,
|
||||||
|
AXUIElementCopyAttributeNames,
|
||||||
|
AXUIElementCopyAttributeValue,
|
||||||
|
)
|
||||||
|
from Cocoa import NSWorkspace
|
||||||
|
|
||||||
|
|
||||||
|
CURSOR_BUNDLE_ID = "com.todesktop.230313mzl4w4u92"
|
||||||
|
|
||||||
|
|
||||||
|
def find_cursor_pid() -> int | None:
|
||||||
|
"""Find the PID of the running Cursor application."""
|
||||||
|
workspace = NSWorkspace.sharedWorkspace()
|
||||||
|
for app in workspace.runningApplications():
|
||||||
|
bundle = app.bundleIdentifier() or ""
|
||||||
|
if bundle == CURSOR_BUNDLE_ID:
|
||||||
|
return app.processIdentifier()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def dump_element(element, depth: int = 0, max_depth: int = 5) -> None:
|
||||||
|
"""Recursively print an AXUIElement's attributes."""
|
||||||
|
if depth > max_depth:
|
||||||
|
return
|
||||||
|
|
||||||
|
indent = " " * depth
|
||||||
|
names_err, attr_names = AXUIElementCopyAttributeNames(element, None)
|
||||||
|
if names_err or not attr_names:
|
||||||
|
return
|
||||||
|
|
||||||
|
role = ""
|
||||||
|
title = ""
|
||||||
|
value = ""
|
||||||
|
description = ""
|
||||||
|
|
||||||
|
for name in attr_names:
|
||||||
|
err, val = AXUIElementCopyAttributeValue(element, name, None)
|
||||||
|
if err:
|
||||||
|
continue
|
||||||
|
if name == "AXRole":
|
||||||
|
role = str(val)
|
||||||
|
elif name == "AXTitle":
|
||||||
|
title = str(val) if val else ""
|
||||||
|
elif name == "AXValue":
|
||||||
|
value_str = str(val)[:100] if val else ""
|
||||||
|
value = value_str
|
||||||
|
elif name == "AXDescription":
|
||||||
|
description = str(val) if val else ""
|
||||||
|
|
||||||
|
label = role
|
||||||
|
if title:
|
||||||
|
label += f' title="{title}"'
|
||||||
|
if description:
|
||||||
|
label += f' desc="{description}"'
|
||||||
|
if value:
|
||||||
|
label += f' value="{value}"'
|
||||||
|
|
||||||
|
print(f"{indent}{label}")
|
||||||
|
|
||||||
|
err, children = AXUIElementCopyAttributeValue(element, "AXChildren", None)
|
||||||
|
if not err and children:
|
||||||
|
for child in children:
|
||||||
|
dump_element(child, depth + 1, max_depth)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Dump Cursor's accessibility tree")
|
||||||
|
parser.add_argument("--depth", type=int, default=8, help="Max depth to traverse")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
pid = find_cursor_pid()
|
||||||
|
if pid is None:
|
||||||
|
print("Cursor is not running.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Found Cursor at PID {pid}")
|
||||||
|
app_element = AXUIElementCreateApplication(pid)
|
||||||
|
dump_element(app_element, max_depth=args.depth)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user