This was a little bit of work, so I wanted to share this example script that crawls the tree AND outputs all attributes. Actually, it would be nice if some of this logic made it into pyax
core.
import json
import re
from typing import Any, Dict
import pyax
from ApplicationServices import (
AXUIElementRef,
AXValueGetType,
AXValueRef,
CFArrayGetTypeID,
CFGetTypeID,
NSPointFromString,
NSRangeFromString,
NSRectFromString,
NSSizeFromString,
kAXValueCFRangeType,
kAXValueCGPointType,
kAXValueCGRectType,
kAXValueCGSizeType,
)
from typeguard import typechecked
@typechecked
def parse_ax_value(value: Any) -> Any:
if value is None:
return value
elif isinstance(value, (bool, int, str, float)):
return value
elif isinstance(value, list) or CFGetTypeID(value) == CFArrayGetTypeID():
return [parse_ax_value(item) for item in value]
elif isinstance(value, AXUIElementRef):
return f"AXUIElement: {repr(value)}"
elif isinstance(value, AXValueRef):
return parse_ax_value_ref(value)
else:
return str(value)
@typechecked
def parse_ax_value_ref(value: AXValueRef) -> Dict[str, Any]:
ax_value_type = AXValueGetType(value)
ax_type_map = {
kAXValueCGSizeType: NSSizeFromString,
kAXValueCGPointType: NSPointFromString,
kAXValueCFRangeType: NSRangeFromString,
kAXValueCGRectType: NSRectFromString,
}
if ax_value_type in ax_type_map:
extracted_str = re.search(r"\{.*\}", str(value)).group()
parsed_value = ax_type_map[ax_value_type](extracted_str)
if ax_value_type == kAXValueCGPointType:
return {"x": parsed_value.x, "y": parsed_value.y}
elif ax_value_type == kAXValueCGSizeType:
return {"width": parsed_value.width, "height": parsed_value.height}
elif ax_value_type == kAXValueCFRangeType:
return {"location": parsed_value.location, "length": parsed_value.length}
elif ax_value_type == kAXValueCGRectType:
return {
"x": parsed_value.origin.x,
"y": parsed_value.origin.y,
"width": parsed_value.size.width,
"height": parsed_value.size.height,
}
else:
return {"unknown_type": str(value)}
@typechecked
def traverse_ui_elements_json(
element: AXUIElementRef, depth: int = 0
) -> Dict[str, Any]:
element_data = {
"pid": element.pid,
"role": element.get_attribute_value("AXRole"),
"subrole": element.get_attribute_value("AXSubrole"),
"title": element.get_attribute_value("AXTitle"),
"attributes": {},
"children": [],
}
try:
for attribute in element.attribute_names:
value = element.get_attribute_value(attribute)
element_data["attributes"][attribute] = parse_ax_value(value)
except Exception as e:
element_data["error"] = f"Error accessing attributes: {e}"
try:
children = element.get_attribute_value("AXChildren")
for child in children or []:
child_data = traverse_ui_elements_json(child, depth + 1)
element_data["children"].append(child_data)
except Exception as e:
element_data["error"] = f"Error accessing children: {e}"
return element_data
if __name__ == "__main__":
import sys
app_name = sys.argv[-1]
acc = pyax.get_application_by_name(app_name)
tree_data = traverse_ui_elements_json(acc)
# Output the data to JSON
print(json.dumps(tree_data, indent=4))