Files
cursor-flasher/scripts/dump_a11y_tree.py
cottongin 2cd48e03f8 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
2026-03-10 02:36:51 -04:00

93 lines
2.5 KiB
Python

"""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()