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
93 lines
2.5 KiB
Python
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()
|