@@ -8,6 +8,10 @@ let kAXActionsAttribute = "AXActions"
8
8
let kAXWindowsAttribute = " AXWindows "
9
9
let kAXPressAction = " AXPress "
10
10
11
+ // Configuration Constants
12
+ let MAX_COLLECT_ALL_HITS = 100000
13
+ let AX_BINARY_VERSION = " 1.0.0 "
14
+
11
15
// Helper function to get AXUIElement type ID
12
16
func AXUIElementGetTypeID( ) -> CFTypeID {
13
17
return AXUIElementGetTypeID_Impl ( )
@@ -257,30 +261,36 @@ func getElementAttributes(_ element: AXUIElement, attributes: [String]) -> Eleme
257
261
}
258
262
else if attr == " AXPosition " || attr == " AXSize " {
259
263
// Handle AXValue types (usually for position and size)
260
- // Safely check if it's an AXValue
261
- let axValueType = AXValueGetType ( unwrappedValue as! AXValue )
262
-
263
- if attr == " AXPosition " && axValueType. rawValue == AXValueType . cgPoint. rawValue {
264
- // It's a position value
265
- var point = CGPoint . zero
266
- if AXValueGetValue ( unwrappedValue as! AXValue , AXValueType . cgPoint, & point) {
267
- extractedValue = [ " x " : Int ( point. x) , " y " : Int ( point. y) ]
268
- } else {
269
- extractedValue = [ " error " : " Position data (conversion failed) " ]
264
+ if CFGetTypeID ( unwrappedValue) == AXValueGetTypeID ( ) {
265
+ let axValue = unwrappedValue as! AXValue
266
+ let axValueType = AXValueGetType ( axValue)
267
+
268
+ if attr == " AXPosition " && axValueType. rawValue == AXValueType . cgPoint. rawValue {
269
+ // It's a position value
270
+ var point = CGPoint . zero
271
+ if AXValueGetValue ( axValue, AXValueType . cgPoint, & point) {
272
+ extractedValue = [ " x " : Int ( point. x) , " y " : Int ( point. y) ]
273
+ } else {
274
+ extractedValue = [ " error " : " Position data (conversion failed) " ]
275
+ }
276
+ }
277
+ else if attr == " AXSize " && axValueType. rawValue == AXValueType . cgSize. rawValue {
278
+ // It's a size value
279
+ var size = CGSize . zero
280
+ if AXValueGetValue ( axValue, AXValueType . cgSize, & size) {
281
+ extractedValue = [ " width " : Int ( size. width) , " height " : Int ( size. height) ]
282
+ } else {
283
+ extractedValue = [ " error " : " Size data (conversion failed) " ]
284
+ }
270
285
}
271
- }
272
- else if attr == " AXSize " && axValueType. rawValue == AXValueType . cgSize. rawValue {
273
- // It's a size value
274
- var size = CGSize . zero
275
- if AXValueGetValue ( unwrappedValue as! AXValue , AXValueType . cgSize, & size) {
276
- extractedValue = [ " width " : Int ( size. width) , " height " : Int ( size. height) ]
277
- } else {
278
- extractedValue = [ " error " : " Size data (conversion failed) " ]
286
+ else {
287
+ // It's some other kind of AXValue
288
+ extractedValue = [ " error " : " AXValue type: \( axValueType. rawValue) " ]
279
289
}
280
- }
281
- else {
282
- // It's some other kind of AXValue
283
- extractedValue = [ " error " : " AXValue type: \( axValueType . rawValue ) " ]
290
+ } else {
291
+ let typeDescriptionCF = CFCopyTypeIDDescription ( CFGetTypeID ( unwrappedValue ) )
292
+ let typeDescription = String ( describing : typeDescriptionCF ?? " Unknown CFType " as CFString )
293
+ extractedValue = [ " error " : " Expected AXValue for attribute \( attr ) but got different type: \( typeDescription ) " ]
284
294
}
285
295
}
286
296
else if attr == " AXTitleUIElement " || attr == " AXLabelUIElement " {
@@ -696,8 +706,8 @@ func collectAll(element: AXUIElement,
696
706
maxDepth: Int = 200 ) {
697
707
698
708
// Safety limit on matches - increased to handle larger web pages
699
- if hits. count > 100000 {
700
- debug ( " Safety limit of 100000 matching elements reached, stopping search " )
709
+ if hits. count > MAX_COLLECT_ALL_HITS {
710
+ debug ( " Safety limit of \( MAX_COLLECT_ALL_HITS ) matching elements reached, stopping search " )
701
711
return
702
712
}
703
713
@@ -868,7 +878,7 @@ func collectAll(element: AXUIElement,
868
878
869
879
let childrenToProcess = uniqueElements. prefix ( maxChildrenToProcess)
870
880
for (i, child) in childrenToProcess. enumerated ( ) {
871
- if hits. count > 100000 { break } // Safety check
881
+ if hits. count > MAX_COLLECT_ALL_HITS { break } // Safety check - Use constant
872
882
873
883
// Safety check - skip this step instead of validating type
874
884
// The AXUIElement type was already validated during collection
@@ -1036,8 +1046,46 @@ func handlePerform(cmd: CommandEnvelope) throws -> PerformResponse {
1036
1046
1037
1047
let decoder = JSONDecoder ( )
1038
1048
let encoder = JSONEncoder ( )
1039
- if #available( macOS 10 . 15 , * ) {
1040
- encoder. outputFormatting = [ . withoutEscapingSlashes]
1049
+ encoder. outputFormatting = [ . withoutEscapingSlashes]
1050
+
1051
+ // Check for command-line arguments like --help before entering main JSON processing loop
1052
+ if CommandLine . arguments. contains ( " --help " ) || CommandLine . arguments. contains ( " -h " ) {
1053
+ // Placeholder for help text
1054
+ // We'll populate this in the next step
1055
+ let helpText = """
1056
+ ax Accessibility Helper v \( AX_BINARY_VERSION)
1057
+
1058
+ This command-line utility interacts with the macOS Accessibility framework.
1059
+ It is typically invoked by a parent process and communicates via JSON on stdin/stdout.
1060
+
1061
+ Usage:
1062
+ <json_command> | ./ax
1063
+
1064
+ Input JSON Command Structure:
1065
+ {
1066
+ " cmd " : " query " | " perform " ,
1067
+ " locator " : {
1068
+ " app " : " <BundleID_or_AppName> " ,
1069
+ " role " : " <AXRole> " ,
1070
+ " match " : { " <Attribute> " : " <Value> " , ... },
1071
+ " pathHint " : [ " <element>[index] " , ...]
1072
+ },
1073
+ " attributes " : [ " <AXAttributeName> " , ...], // For cmd: " query "
1074
+ " action " : " <AXActionName> " , // For cmd: " perform "
1075
+ " multi " : true | false, // For cmd: " query " , to get all matches
1076
+ " requireAction " : " <AXActionName> " // For cmd: " query " , filter by action support
1077
+ }
1078
+
1079
+ Example Query:
1080
+ echo '{ " cmd " : " query " , " locator " :{ " app " : " Safari " , " role " : " AXWindow " , " match " :{ " AXMain " : " true " }}, " attributes " :[ " AXTitle " ]}' | ./ax
1081
+
1082
+ Permissions:
1083
+ Ensure the application that executes 'ax' (e.g., Terminal, an IDE, or a Node.js process)
1084
+ has 'Accessibility' permissions enabled in:
1085
+ System Settings > Privacy & Security > Accessibility.
1086
+ """
1087
+ print ( helpText)
1088
+ exit ( 0 )
1041
1089
}
1042
1090
1043
1091
// Check for accessibility permissions before starting
0 commit comments