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