Skip to content

Commit

Permalink
Add python dump_tree example for Mac (#211)
Browse files Browse the repository at this point in the history
* Add python dump_tree example for Mac

* Add extra attributes to test

* Fix issues with getting certain attributes in Python on Mac
  • Loading branch information
alice committed Apr 24, 2024
1 parent ace3ce0 commit 18d1c86
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 16 deletions.
18 changes: 18 additions & 0 deletions examples/mac/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,21 @@ if (ACACIA_NODEJS)
VERBATIM
)
endif (ACACIA_NODEJS)

if (ACACIA_PYTHON)
add_custom_target(
dump_tree_axapi.py
ALL
DEPENDS
acacia_axapi
python_acacia_axapi
COMMAND
${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/dump_tree_axapi.py ${CMAKE_CURRENT_BINARY_DIR}/dump_tree_axapi.py
COMMAND
${CMAKE_COMMAND} -E create_symlink ${CMAKE_BINARY_DIR}/lib/mac/_python_acacia_axapi.so ${CMAKE_CURRENT_BINARY_DIR}/_python_acacia_axapi.so
COMMAND
${CMAKE_COMMAND} -E create_symlink ${CMAKE_BINARY_DIR}/lib/mac/acacia_axapi.py ${CMAKE_CURRENT_BINARY_DIR}/acacia_axapi.py

VERBATIM
)
endif (ACACIA_PYTHON)
154 changes: 154 additions & 0 deletions examples/mac/dump_tree_axapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/env python3

import argparse
import sys
import acacia_axapi

ATTRIBUTES = [
'AXRoleDescription',
'AXTitle',
'AXDescription',
'AXFocused',
'AXDOMClassList',
'AXSelectedTextRanges',
]


def print_node_and_subtree(node, level):
prefix = "--" * level
all_attributes =
role_and_attributes = serialize_role_and_attributes(node, ATTRIBUTES)
print(f'{prefix} {role_and_attributes}')

if not node.hasAttribute('AXChildren'):
return

children = get_list_attribute_value(node, 'AXChildren')
if children is None:
return

for child in children:
print_node_and_subtree(child, level + 1)


def serialize_role_and_attributes(node, attributes):
output = get_attribute_value(node, 'AXRole')

for attribute in attributes:
if not node.hasAttribute(attribute):
continue

value = get_attribute_value(node, attribute)
match type(value).__name__:
case 'NoneType':
value_type = node.getValueType(attribute)
if value_type is acacia_axapi.ValueType_UNKNOWN:
output += f' {attribute}=(unknown)'
continue
case 'bool':
if value:
output += f' {attribute}'
case 'str':
value_utf16 = value.encode('utf-16', 'surrogatepass').decode('utf-16', 'replace')
output += f' {attribute}="{value_utf16}"'
case _:
output += f' {attribute}={value}'

return output


def get_attribute_value(node, attribute):
try:
type = node.getValueType(attribute)
match type:
case acacia_axapi.ValueType_LIST:
return get_list_attribute_value(node, attribute)
case acacia_axapi.ValueType_BOOLEAN:
return node.getBooleanValue(attribute)
case acacia_axapi.ValueType_INT:
return node.getIntValue(attribute)
case acacia_axapi.ValueType_FLOAT:
return node.getFloatValue(attribute)
case acacia_axapi.ValueType_STRING:
return node.getStringValue(attribute)
case acacia_axapi.ValueType_URL:
return node.getURLValue(attribute)
case acacia_axapi.ValueType_NODE:
return node.getNodeValue(attribute)
case acacia_axapi.ValueType_POINT:
return node.getPointValue(attribute)
case acacia_axapi.ValueType_SIZE:
return node.getSizeValue(attribute)
case acacia_axapi.ValueType_RECT:
return node.getRectValue(attribute)
case acacia_axapi.ValueType_RANGE:
return node.getRangeValue(attribute)
case acacia_axapi.ValueType_DICTIONARY:
return node.getDictionaryValue(attribute)
case _:
return None
except Exception as error:
return f'(error: {error})'


def get_list_attribute_value(node, attribute):
count = node.getListElementCount(attribute)
if count == 0:
return []

type = node.getListElementType(attribute)
list = None
match type:
case acacia_axapi.ValueType_NODE:
list = node.getNodeListValue(attribute)
case acacia_axapi.ValueType_STRING:
list = node.getStringListValue(attribute)
case acacia_axapi.ValueType_RANGE:
list = node.getRangeListValue(attribute)
case acacia_axapi.ValueType_DICTIONARY:
list = node.getDictionaryListValue(attribute)
case _:
type_string = acacia_axapi.ValueTypeToString(type)
sys.stderr.write(f'Unsupported list type: {type_string} for {attribute}.')
return None

result = []
for i in range(count):
result.append(list[i])
return result


def process_arguments():
parser = argparse.ArgumentParser(
description=
"Dumps the Mac accessibility tree for the specified application.")
parser.add_argument("--pid", type=int)
parser.add_argument("--name", type=str)
args = parser.parse_args()
if not (args.pid or args.name):
parser.print_help()
sys.exit()
return args


def main():
args = process_arguments()
if args.name:
print(f'Searching for application with name "{args.name}".')
root = acacia_axapi.findRootAXAPINodeForName(args.name)
else:
print(f'Searching for application with pid {args.pid}.')
root = acacia_axapi.findRootAXAPINodeForPID(args.pid)

if root.isNull():
sys.stderr.write("Error: Application not found.")
sys.exit()

try:
print_node_and_subtree(root, 0)
except Exception as error:
sys.stderr.write(f"Error encountered printing node: {error}")


if __name__ == "__main__":
main()
18 changes: 10 additions & 8 deletions include/acacia/mac/axapi_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ AXAPINode findRootAXAPINodeForPID(int pid);
* application whose name matches the given name, or a null AXAPINode if no such
* application exists.
*/
AXAPINode findRootAXAPINodeForName(std::string name);
AXAPINode findRootAXAPINodeForName(const std::string& name);

/**
* Represents a node in the macOS (AX API) accessibility tree.
Expand Down Expand Up @@ -265,7 +265,8 @@ class AXAPINode {
* elements are ValueType::STRING values.
* @ingroup axapi
*/
std::vector<std::string> getStringListValue(std::string& attribute) const;
std::vector<std::string> getStringListValue(
const std::string& attribute) const;

/**
* Wraps
Expand All @@ -275,7 +276,7 @@ class AXAPINode {
* is a ValueType::LIST whose elements are ValueType::STRING values.
* @ingroup axapi
*/
std::string getStringListValueAtIndex(std::string& attribute,
std::string getStringListValueAtIndex(const std::string& attribute,
int index) const;

/**
Expand All @@ -286,7 +287,7 @@ class AXAPINode {
* elements are ValueType::RANGE values.
* @ingroup axapi
*/
std::vector<Range> getRangeListValue(std::string& attribute) const;
std::vector<Range> getRangeListValue(const std::string& attribute) const;

/**
* Wraps
Expand All @@ -296,7 +297,7 @@ class AXAPINode {
* is a ValueType::LIST whose elements are ValueType::RANGE values.
* @ingroup axapi
*/
Range getRangeListValueAtIndex(std::string& attribute, int index) const;
Range getRangeListValueAtIndex(const std::string& attribute, int index) const;

/**
* Wraps
Expand All @@ -306,7 +307,8 @@ class AXAPINode {
* elements are ValueType::DICTIONARY values.
* @ingroup axapi
*/
std::vector<Dictionary> getDictionaryListValue(std::string& attribute) const;
std::vector<Dictionary> getDictionaryListValue(
const std::string& attribute) const;

/**
* Wraps
Expand All @@ -316,11 +318,11 @@ class AXAPINode {
* is a ValueType::LIST whose elements are ValueType::DICTIONARY values.
* @ingroup axapi
*/
Dictionary getDictionaryListValueAtIndex(std::string& attribute,
Dictionary getDictionaryListValueAtIndex(const std::string& attribute,
int index) const;

friend AXAPINode findRootAXAPINodeForPID(int pid);
friend AXAPINode findRootAXAPINodeForName(std::string name);
friend AXAPINode findRootAXAPINodeForName(const std::string& name);

private:
explicit AXAPINode(AXUIElementRef ax_element);
Expand Down
18 changes: 10 additions & 8 deletions lib/mac/axapi_node.mm
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ AXAPINode findRootAXAPINodeForPID(int pid) {
return AXAPINode(root_ax_ui_element);
}

AXAPINode findRootAXAPINodeForName(std::string name) {
AXAPINode findRootAXAPINodeForName(const std::string& name) {
ScopedCFTypeRef<CFStringRef> cf_name = StdStringToCFStringRef(name);
NSWorkspace* ws = [NSWorkspace sharedWorkspace];
NSArray* running_apps = [[ws runningApplications]
Expand Down Expand Up @@ -447,7 +447,7 @@ AXAPINode findRootAXAPINodeForName(std::string name) {
}

std::vector<std::string> AXAPINode::getStringListValue(
std::string& attribute) const {
const std::string& attribute) const {
ScopedCFTypeRef<CFArrayRef> cf_array = GetRawArrayValue(attribute);

std::vector<std::string> value;
Expand All @@ -468,15 +468,16 @@ AXAPINode findRootAXAPINodeForName(std::string name) {
return value;
}

std::string AXAPINode::getStringListValueAtIndex(std::string& attribute,
std::string AXAPINode::getStringListValueAtIndex(const std::string& attribute,
int index) const {
auto cf_value =
GetRawArrayValueAtIndex<CFStringRef>(attribute, index, ValueType::STRING);

return CFStringRefToStdString(cf_value.get());
}

std::vector<Range> AXAPINode::getRangeListValue(std::string& attribute) const {
std::vector<Range> AXAPINode::getRangeListValue(
const std::string& attribute) const {
ScopedCFTypeRef<CFArrayRef> cf_array = GetRawArrayValue(attribute);

std::vector<Range> value;
Expand Down Expand Up @@ -505,7 +506,7 @@ AXAPINode findRootAXAPINodeForName(std::string name) {
return value;
}

Range AXAPINode::getRangeListValueAtIndex(std::string& attribute,
Range AXAPINode::getRangeListValueAtIndex(const std::string& attribute,
int index) const {
auto cf_value =
GetRawArrayValueAtIndex<AXValueRef>(attribute, index, ValueType::RANGE);
Expand All @@ -520,7 +521,7 @@ AXAPINode findRootAXAPINodeForName(std::string name) {
}

std::vector<Dictionary> AXAPINode::getDictionaryListValue(
std::string& attribute) const {
const std::string& attribute) const {
ScopedCFTypeRef<CFArrayRef> cf_array = GetRawArrayValue(attribute);

std::vector<Dictionary> value;
Expand All @@ -542,8 +543,9 @@ AXAPINode findRootAXAPINodeForName(std::string name) {
return value;
}

Dictionary AXAPINode::getDictionaryListValueAtIndex(std::string& attribute,
int index) const {
Dictionary AXAPINode::getDictionaryListValueAtIndex(
const std::string& attribute,
int index) const {
auto cf_value = GetRawArrayValueAtIndex<CFDictionaryRef>(
attribute, index, ValueType::DICTIONARY);

Expand Down

0 comments on commit 18d1c86

Please sign in to comment.