Lerge refactor

This commit is contained in:
Peter Steinberger 2025-05-23 01:09:55 +02:00
parent 316d8145f5
commit ffd784a61d
54 changed files with 3360 additions and 3102 deletions

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"configurations": [
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:AXorcist}",
"name": "Debug axorc",
"program": "${workspaceFolder:AXorcist}/.build/debug/axorc",
"preLaunchTask": "swift: Build Debug axorc"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:AXorcist}",
"name": "Release axorc",
"program": "${workspaceFolder:AXorcist}/.build/release/axorc",
"preLaunchTask": "swift: Build Release axorc"
}
]
}

View File

@ -1,41 +1,5 @@
{
"pins" : [
{
"identity" : "defaults",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/Defaults",
"state" : {
"revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6",
"version" : "7.3.1"
}
},
{
"identity" : "keyboardshortcuts",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/KeyboardShortcuts",
"state" : {
"revision" : "045cf174010beb335fa1d2567d18c057b8787165",
"version" : "2.3.0"
}
},
{
"identity" : "launchatlogin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin",
"state" : {
"revision" : "9a894d799269cb591037f9f9cb0961510d4dca81",
"version" : "5.0.2"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
"revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99",
"version" : "2.7.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
@ -45,15 +9,6 @@
"version" : "1.5.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa",
"version" : "1.6.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
@ -71,15 +26,6 @@
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
"version" : "0.99.0"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"revision" : "121c146fe591b1320238d054ae35c81ffa45f45a",
"version" : "0.12.0"
}
}
],
"version" : 2

View File

@ -39,7 +39,7 @@ extension AXorcist {
var cfValue: CFTypeRef?
let copyAttributeStatus = AXUIElementCopyAttributeValue(
appElement.underlyingElement,
kAXFocusedUIElementAttribute as CFString,
AXAttributeNames.kAXFocusedUIElementAttribute as CFString,
&cfValue
)

View File

@ -2,8 +2,7 @@ import AppKit
import ApplicationServices
import Foundation
// Global constant for backwards compatibility
public let DEFAULT_MAX_DEPTH_SEARCH = 10
// Global constant for backwards compatibility - removed, now using AXMiscConstants.defaultMaxDepthSearch
// Placeholder for the actual accessibility logic.
// For now, this module is very thin and AXorcist.swift is the main public API.
@ -15,8 +14,8 @@ public class AXorcist {
internal var recursiveCallDebugLogs: [String] = [] // Added for recursive logging
// Default values for collection and search if not provided by the command
public static let defaultMaxDepthSearch = 10 // Default for general locator-based searches
public static let defaultMaxDepthCollectAll = 7 // Default for collectAll recursive operations
public static let defaultMaxDepthCollectAll = 7 // Default recursion depth for collectAll
public static let defaultMaxDepthSearch = 15 // Default recursion depth for search operations
public static let defaultMaxDepthPathResolution = 15 // Max depth for resolving path hints
public static let defaultMaxDepthDescribe = 5 // ADDED: Default for description recursion
public static let defaultTimeoutPerElementCollectAll = 0.5 // seconds
@ -70,166 +69,36 @@ public class AXorcist {
// MARK: - Path Navigation
// Helper to check if the current element matches a specific attribute-value pair
@MainActor
private static func currentElementMatchesPathComponent(
_ element: Element,
attributeName: String,
expectedValue: String,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String] // For logging
) -> Bool {
// Helper to log directly to currentDebugLogs for this function
func logLocal(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
if isDebugLoggingEnabled { // Check if logging is enabled for this specific call context
let logMessage = AXorcist.formatDebugLogMessage(
message,
applicationName: nil, // Or pass from navigateToElement if needed
commandID: nil, // Or pass from navigateToElement if needed
file: file,
function: function,
line: line
)
currentDebugLogs.append(logMessage)
}
}
// MARK: - Search Operations
if attributeName.isEmpty { // Should not happen if parsePathComponent is robust
logLocal("currentElementMatchesPathComponent: attributeName is empty, cannot match.")
return false
}
if let actualValue = element.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
// logLocal("currentElementMatchesPathComponent: Element \(element.briefDescription(option: .minimal, isDebugLoggingEnabled: false, currentDebugLogs: &currentDebugLogs)) has '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
return true
}
}
return false
}
// Updated navigateToElement to prioritize children
@MainActor
internal func navigateToElement(
from startElement: Element,
pathHint: [String],
public func search(
element: Element,
locator: Locator,
requireAction: String?,
depth: Int,
maxDepth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Element? {
let pathHintString = pathHint.joined(separator: ", ")
// Log with the actual isDebugLoggingEnabled value
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Entered. isDebugLoggingEnabled: \(isDebugLoggingEnabled). pathHint: [\(pathHintString)]", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
func dLog(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
// Use the passed-in isDebugLoggingEnabled
if isDebugLoggingEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: file, function: function, line: line))
}
) -> (foundElement: Element?, logs: [String]) {
// Initial log for this AXorcist-level search call
if isDebugLoggingEnabled {
let initialMessage = "AXorcist.search called with locator: \(locator.criteria), path_hint: \(locator.root_element_path_hint ?? [])"
currentDebugLogs.append(AXorcist.formatDebugLogMessage(initialMessage, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
var currentElement = startElement
var currentPathSegmentForLog = ""
// Call the global findElementViaPathAndCriteria
// Note: findElementViaPathAndCriteria will handle its own detailed logging (dLog to currentDebugLogs if !JSON_LOG, or writeSearchLogEntry to stderr if JSON_LOG)
let foundElement = findElementViaPathAndCriteria(
application: element,
locator: locator,
maxDepth: maxDepth, // Assuming 'depth' passed to AXorcist.search is for initial call, maxDepth for traversal
isDebugLoggingEnabledParam: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass this along for findElementViaPathAndCriteria to use
)
for (index, pathComponentString) in pathHint.enumerated() {
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)")
let (attributeName, expectedValue) = PathUtils.parsePathComponent(pathComponentString)
guard !attributeName.isEmpty else {
dLog("CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty attribute name from pathComponentString '\(pathComponentString)'")
return nil
}
var foundMatchForThisComponent = false
var newElementForNextStep: Element? = nil
// Priority 1: Check children using Element.children()
if let childrenFromElementDotChildren = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)")
for child in childrenFromElementDotChildren {
let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let actualValue = child.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog(" [Nav Child Check 1] Child: \(childBriefDescForLog), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
dLog("Matched child (from Element.children): \(childBriefDescForLog) for '\(attributeName):\(expectedValue)'")
newElementForNextStep = child
foundMatchForThisComponent = true
break
}
} else {
// dLog("Attribute '\(attributeName)' was nil for child (from Element.children): \(childBriefDescForLog)")
}
}
} else {
dLog("Current element \(briefDesc) has no children from Element.children() or children array was nil.")
}
// FALLBACK: If no child matched via Element.children(), try direct kAXChildrenAttribute call (Heisenbug workaround)
if !foundMatchForThisComponent {
// Log entry for this fallback block, without using currentElement.briefDescription() before the critical call.
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: No match from Element.children(). Trying direct kAXChildrenAttribute fallback.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
var directChildrenValue: CFTypeRef?
let directChildrenError = AXUIElementCopyAttributeValue(currentElement.underlyingElement, kAXChildrenAttribute as CFString, &directChildrenValue)
// Now, after the critical call, we can get the description for logging.
let currentElementDescForFallbackLog = isDebugLoggingEnabled ? currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) : "Element(debug_off)"
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Fallback is for element: \(currentElementDescForFallbackLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if directChildrenError == .success, let cfArray = directChildrenValue, CFGetTypeID(cfArray) == CFArrayGetTypeID() {
if let directAxElements = cfArray as? [AXUIElement] {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback found \(directAxElements.count) raw children for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
for axChild in directAxElements {
let childElement = Element(axChild)
let childBriefDescForLogFallback = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let actualValue = childElement.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog(" [Nav Child Check 2-Fallback] Child: \(childBriefDescForLogFallback), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Matched child (from direct fallback) for '\(attributeName):\(expectedValue)' on \(currentElementDescForFallbackLog). Child: \(childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
newElementForNextStep = childElement
foundMatchForThisComponent = true
break
}
}
}
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: CFArray failed to cast to [AXUIElement] for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
} else if directChildrenError != .success {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: Error fetching for \(currentElementDescForFallbackLog): \(directChildrenError.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: No children or not an array for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// Priority 2: If no child matched (even after fallback), check current element itself
if !foundMatchForThisComponent {
var tempLogsForMatchCheck = currentDebugLogs
let matchResult = AXorcist.currentElementMatchesPathComponent(
currentElement,
attributeName: attributeName,
expectedValue: expectedValue,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForMatchCheck
)
currentDebugLogs = tempLogsForMatchCheck
if matchResult {
dLog("Current element \(briefDesc) itself matches '\(attributeName):\(expectedValue)'. Retaining current element for this step.")
newElementForNextStep = currentElement
foundMatchForThisComponent = true
}
}
if foundMatchForThisComponent, let nextElement = newElementForNextStep {
currentElement = nextElement
} else {
dLog("Neither current element \(briefDesc) nor its children (after all checks) matched '\(attributeName):\(expectedValue)'. Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER")
return nil
}
}
dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
return currentElement
// The currentDebugLogs array has been populated by findElementViaPathAndCriteria (if JSON logging is off)
// or contains only the initial logs from this function if JSON logging is on.
return (foundElement: foundElement, logs: currentDebugLogs)
}
}

View File

@ -0,0 +1,17 @@
// AXActionNameConstants.swift - Accessibility action name constants
import Foundation
public enum AXActionNames {
public static let kAXIncrementAction = "AXIncrement" // New
public static let kAXDecrementAction = "AXDecrement" // New
public static let kAXConfirmAction = "AXConfirm" // New
public static let kAXCancelAction = "AXCancel" // New
public static let kAXShowMenuAction = "AXShowMenu"
public static let kAXPickAction = "AXPick" // New (Obsolete in headers, but sometimes seen)
public static let kAXPressAction = "AXPress" // New
public static let kAXRaiseAction = "AXRaise" // New
// Specific action name for setting a value, used internally by performActionOnElement
public static let kAXSetValueAction = "AXSetValue"
}

View File

@ -0,0 +1,112 @@
// AXAttributeNameConstants.swift - Accessibility attribute name constants
import Foundation
public enum AXAttributeNames {
// Standard Accessibility Attributes - Values should match CFSTR defined in AXAttributeConstants.h
public static let kAXRoleAttribute = "AXRole" // Reverted to String literal
public static let kAXSubroleAttribute = "AXSubrole"
public static let kAXRoleDescriptionAttribute = "AXRoleDescription"
public static let kAXTitleAttribute = "AXTitle"
public static let kAXValueAttribute = "AXValue"
public static let kAXValueDescriptionAttribute = "AXValueDescription" // New
public static let kAXDescriptionAttribute = "AXDescription"
public static let kAXHelpAttribute = "AXHelp"
public static let kAXIdentifierAttribute = "AXIdentifier"
public static let kAXPlaceholderValueAttribute = "AXPlaceholderValue"
public static let kAXLabelUIElementAttribute = "AXLabelUIElement"
public static let kAXTitleUIElementAttribute = "AXTitleUIElement"
public static let kAXLabelValueAttribute = "AXLabelValue"
public static let kAXElementBusyAttribute = "AXElementBusy" // New
public static let kAXAlternateUIVisibleAttribute = "AXAlternateUIVisible" // New
public static let kAXChildrenAttribute = "AXChildren"
public static let kAXParentAttribute = "AXParent"
public static let kAXWindowsAttribute = "AXWindows"
public static let kAXMainWindowAttribute = "AXMainWindow"
public static let kAXFocusedWindowAttribute = "AXFocusedWindow"
public static let kAXFocusedUIElementAttribute = "AXFocusedUIElement"
public static let kAXEnabledAttribute = "AXEnabled"
public static let kAXFocusedAttribute = "AXFocused"
public static let kAXMainAttribute = "AXMain" // Window-specific
public static let kAXMinimizedAttribute = "AXMinimized" // New, Window-specific
public static let kAXCloseButtonAttribute = "AXCloseButton" // New, Window-specific
public static let kAXZoomButtonAttribute = "AXZoomButton" // New, Window-specific
public static let kAXMinimizeButtonAttribute = "AXMinimizeButton" // New, Window-specific
public static let kAXFullScreenButtonAttribute = "AXFullScreenButton" // New, Window-specific
public static let kAXDefaultButtonAttribute = "AXDefaultButton" // New, Window-specific
public static let kAXCancelButtonAttribute = "AXCancelButton" // New, Window-specific
public static let kAXGrowAreaAttribute = "AXGrowArea" // New, Window-specific
public static let kAXModalAttribute = "AXModal" // New, Window-specific
public static let kAXMenuBarAttribute = "AXMenuBar" // New, App-specific
public static let kAXFrontmostAttribute = "AXFrontmost" // New, App-specific
public static let kAXHiddenAttribute = "AXHidden" // New, App-specific
public static let kAXPositionAttribute = "AXPosition"
public static let kAXSizeAttribute = "AXSize"
// Value attributes
public static let kAXMinValueAttribute = "AXMinValue" // New
public static let kAXMaxValueAttribute = "AXMaxValue" // New
public static let kAXValueIncrementAttribute = "AXValueIncrement" // New
public static let kAXAllowedValuesAttribute = "AXAllowedValues" // New
// Text-specific attributes
public static let kAXSelectedTextAttribute = "AXSelectedText" // New
public static let kAXSelectedTextRangeAttribute = "AXSelectedTextRange" // New
public static let kAXNumberOfCharactersAttribute = "AXNumberOfCharacters" // New
public static let kAXVisibleCharacterRangeAttribute = "AXVisibleCharacterRange" // New
public static let kAXInsertionPointLineNumberAttribute = "AXInsertionPointLineNumber" // New
// Actions - Values should match CFSTR defined in AXActionConstants.h
public static let kAXActionsAttribute = "AXActions" // This is actually kAXActionNamesAttribute typically
public static let kAXActionNamesAttribute = "AXActionNames" // Correct name for listing actions
public static let kAXActionDescriptionAttribute =
"AXActionDescription" // To get desc of an action (not in AXActionConstants.h but AXUIElement.h)
// Attributes for web content and tables/lists
public static let kAXVisibleChildrenAttribute = "AXVisibleChildren"
public static let kAXSelectedChildrenAttribute = "AXSelectedChildren"
public static let kAXTabsAttribute = "AXTabs" // Often a kAXRadioGroup or kAXTabGroup role
public static let kAXRowsAttribute = "AXRows"
public static let kAXColumnsAttribute = "AXColumns"
public static let kAXSelectedRowsAttribute = "AXSelectedRows" // New
public static let kAXSelectedColumnsAttribute = "AXSelectedColumns" // New
public static let kAXIndexAttribute = "AXIndex" // New (for rows/columns)
public static let kAXDisclosingAttribute = "AXDisclosing" // New (for outlines)
// Custom or less standard attributes (verify usage and standard names)
public static let kAXPathHintAttribute = "AXPathHint" // Our custom attribute for pathing
// DOM specific attributes (these seem custom or web-specific, not standard Apple AX)
// Verify if these are actual attribute names exposed by web views or custom implementations.
public static let kAXDOMIdentifierAttribute = "AXDOMIdentifier" // Example, might not be standard AX
public static let kAXDOMClassListAttribute = "AXDOMClassList" // Example, might not be standard AX
public static let kAXARIADOMResourceAttribute = "AXARIADOMResource" // Example
public static let kAXARIADOMFunctionAttribute = "AXARIADOM-función" // Corrected identifier, kept original string value.
public static let kAXARIADOMChildrenAttribute = "AXARIADOMChildren" // New
public static let kAXDOMChildrenAttribute = "AXDOMChildren" // New
// New constants for missing attributes
public static let kAXToolbarButtonAttribute = "AXToolbarButton"
public static let kAXProxyAttribute = "AXProxy"
public static let kAXSelectedCellsAttribute = "AXSelectedCells"
public static let kAXHeaderAttribute = "AXHeader"
public static let kAXHorizontalScrollBarAttribute = "AXHorizontalScrollBar"
public static let kAXVerticalScrollBarAttribute = "AXVerticalScrollBar"
// Attributes used in child heuristic collection (often non-standard or specific)
public static let kAXWebAreaChildrenAttribute = "AXWebAreaChildren"
public static let kAXHTMLContentAttribute = "AXHTMLContent"
public static let kAXApplicationNavigationAttribute = "AXApplicationNavigation"
public static let kAXApplicationElementsAttribute = "AXApplicationElements"
public static let kAXContentsAttribute = "AXContents"
public static let kAXBodyAreaAttribute = "AXBodyArea"
public static let kAXDocumentContentAttribute = "AXDocumentContent"
public static let kAXWebPageContentAttribute = "AXWebPageContent"
public static let kAXSplitGroupContentsAttribute = "AXSplitGroupContents"
public static let kAXLayoutAreaChildrenAttribute = "AXLayoutAreaChildren"
public static let kAXGroupChildrenAttribute = "AXGroupChildren"
}

View File

@ -0,0 +1,23 @@
// AXMiscConstants.swift - Miscellaneous accessibility constants
import Foundation
public enum AXMiscConstants {
// Configuration Constants
public static let maxCollectAllHits = 200 // Default max elements for collect_all if not specified in command
public static let defaultMaxDepthSearch = 20 // Default max recursion depth for search
public static let defaultMaxDepthCollectAll = 15 // Default max recursion depth for collect_all
public static let axBinaryVersion = "1.1.7" // Updated version
public static let binaryVersion = "1.1.7" // Updated version without AX prefix
// String constant for "not available"
public static let kAXNotAvailableString = "n/a"
// MARK: - Custom Application/Computed Keys
public static let focusedApplicationKey = "focused"
public static let computedNameAttributeKey = "ComputedName"
public static let isClickableAttributeKey = "IsClickable"
public static let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
public static let computedPathAttributeKey = "ComputedPath"
}

View File

@ -0,0 +1,43 @@
// AXRoleNameConstants.swift - Accessibility role name constants
import Foundation
public enum AXRoleNames {
// Standard Accessibility Roles - Values should match CFSTR defined in AXRoleConstants.h (examples, add more as needed)
public static let kAXApplicationRole = "AXApplication"
public static let kAXSystemWideRole = "AXSystemWide" // New
public static let kAXWindowRole = "AXWindow"
public static let kAXSheetRole = "AXSheet" // New
public static let kAXDrawerRole = "AXDrawer" // New
public static let kAXGroupRole = "AXGroup"
public static let kAXButtonRole = "AXButton"
public static let kAXRadioButtonRole = "AXRadioButton" // New
public static let kAXCheckBoxRole = "AXCheckBox"
public static let kAXPopUpButtonRole = "AXPopUpButton" // New
public static let kAXMenuButtonRole = "AXMenuButton" // New
public static let kAXStaticTextRole = "AXStaticText"
public static let kAXTextFieldRole = "AXTextField"
public static let kAXTextAreaRole = "AXTextArea"
public static let kAXScrollAreaRole = "AXScrollArea"
public static let kAXScrollBarRole = "AXScrollBar" // New
public static let kAXWebAreaRole = "AXWebArea"
public static let kAXImageRole = "AXImage" // New
public static let kAXListRole = "AXList" // New
public static let kAXTableRole = "AXTable" // New
public static let kAXOutlineRole = "AXOutline" // New
public static let kAXColumnRole = "AXColumn" // New
public static let kAXRowRole = "AXRow" // New
public static let kAXToolbarRole = "AXToolbar"
public static let kAXBusyIndicatorRole = "AXBusyIndicator" // New
public static let kAXProgressIndicatorRole = "AXProgressIndicator" // New
public static let kAXSliderRole = "AXSlider" // New
public static let kAXIncrementorRole = "AXIncrementor" // New
public static let kAXDisclosureTriangleRole = "AXDisclosureTriangle" // New
public static let kAXMenuRole = "AXMenu" // New
public static let kAXMenuItemRole = "AXMenuItem" // New
public static let kAXSplitGroupRole = "AXSplitGroup" // New
public static let kAXSplitterRole = "AXSplitter" // New
public static let kAXColorWellRole = "AXColorWell" // New
public static let kAXLinkRole = "AXLink" // Added for consistency
public static let kAXUnknownRole = "AXUnknown" // New
}

View File

@ -1,203 +0,0 @@
// AccessibilityConstants.swift - Defines global constants used throughout the accessibility helper
import AppKit // Added for NSAccessibility
import ApplicationServices // Added for AXError type
import Foundation
// Configuration Constants
public let maxCollectAllHits = 200 // Default max elements for collect_all if not specified in command
public let defaultMaxDepthSearch = 20 // Default max recursion depth for search
public let defaultMaxDepthCollectAll = 15 // Default max recursion depth for collect_all
public let axBinaryVersion = "1.1.7" // Updated version
public let binaryVersion = "1.1.7" // Updated version without AX prefix
// Standard Accessibility Attributes - Values should match CFSTR defined in AXAttributeConstants.h
public let kAXRoleAttribute = "AXRole" // Reverted to String literal
public let kAXSubroleAttribute = "AXSubrole"
public let kAXRoleDescriptionAttribute = "AXRoleDescription"
public let kAXTitleAttribute = "AXTitle"
public let kAXValueAttribute = "AXValue"
public let kAXValueDescriptionAttribute = "AXValueDescription" // New
public let kAXDescriptionAttribute = "AXDescription"
public let kAXHelpAttribute = "AXHelp"
public let kAXIdentifierAttribute = "AXIdentifier"
public let kAXPlaceholderValueAttribute = "AXPlaceholderValue"
public let kAXLabelUIElementAttribute = "AXLabelUIElement"
public let kAXTitleUIElementAttribute = "AXTitleUIElement"
public let kAXLabelValueAttribute = "AXLabelValue"
public let kAXElementBusyAttribute = "AXElementBusy" // New
public let kAXAlternateUIVisibleAttribute = "AXAlternateUIVisible" // New
public let kAXChildrenAttribute = "AXChildren"
public let kAXParentAttribute = "AXParent"
public let kAXWindowsAttribute = "AXWindows"
public let kAXMainWindowAttribute = "AXMainWindow"
public let kAXFocusedWindowAttribute = "AXFocusedWindow"
public let kAXFocusedUIElementAttribute = "AXFocusedUIElement"
public let kAXEnabledAttribute = "AXEnabled"
public let kAXFocusedAttribute = "AXFocused"
public let kAXMainAttribute = "AXMain" // Window-specific
public let kAXMinimizedAttribute = "AXMinimized" // New, Window-specific
public let kAXCloseButtonAttribute = "AXCloseButton" // New, Window-specific
public let kAXZoomButtonAttribute = "AXZoomButton" // New, Window-specific
public let kAXMinimizeButtonAttribute = "AXMinimizeButton" // New, Window-specific
public let kAXFullScreenButtonAttribute = "AXFullScreenButton" // New, Window-specific
public let kAXDefaultButtonAttribute = "AXDefaultButton" // New, Window-specific
public let kAXCancelButtonAttribute = "AXCancelButton" // New, Window-specific
public let kAXGrowAreaAttribute = "AXGrowArea" // New, Window-specific
public let kAXModalAttribute = "AXModal" // New, Window-specific
public let kAXMenuBarAttribute = "AXMenuBar" // New, App-specific
public let kAXFrontmostAttribute = "AXFrontmost" // New, App-specific
public let kAXHiddenAttribute = "AXHidden" // New, App-specific
public let kAXPositionAttribute = "AXPosition"
public let kAXSizeAttribute = "AXSize"
// Value attributes
public let kAXMinValueAttribute = "AXMinValue" // New
public let kAXMaxValueAttribute = "AXMaxValue" // New
public let kAXValueIncrementAttribute = "AXValueIncrement" // New
public let kAXAllowedValuesAttribute = "AXAllowedValues" // New
// Text-specific attributes
public let kAXSelectedTextAttribute = "AXSelectedText" // New
public let kAXSelectedTextRangeAttribute = "AXSelectedTextRange" // New
public let kAXNumberOfCharactersAttribute = "AXNumberOfCharacters" // New
public let kAXVisibleCharacterRangeAttribute = "AXVisibleCharacterRange" // New
public let kAXInsertionPointLineNumberAttribute = "AXInsertionPointLineNumber" // New
// Actions - Values should match CFSTR defined in AXActionConstants.h
public let kAXActionsAttribute = "AXActions" // This is actually kAXActionNamesAttribute typically
public let kAXActionNamesAttribute = "AXActionNames" // Correct name for listing actions
public let kAXActionDescriptionAttribute =
"AXActionDescription" // To get desc of an action (not in AXActionConstants.h but AXUIElement.h)
public let kAXIncrementAction = "AXIncrement" // New
public let kAXDecrementAction = "AXDecrement" // New
public let kAXConfirmAction = "AXConfirm" // New
public let kAXCancelAction = "AXCancel" // New
public let kAXShowMenuAction = "AXShowMenu"
public let kAXPickAction = "AXPick" // New (Obsolete in headers, but sometimes seen)
public let kAXPressAction = "AXPress" // New
// Specific action name for setting a value, used internally by performActionOnElement
public let kAXSetValueAction = "AXSetValue"
// Standard Accessibility Roles - Values should match CFSTR defined in AXRoleConstants.h (examples, add more as needed)
public let kAXApplicationRole = "AXApplication"
public let kAXSystemWideRole = "AXSystemWide" // New
public let kAXWindowRole = "AXWindow"
public let kAXSheetRole = "AXSheet" // New
public let kAXDrawerRole = "AXDrawer" // New
public let kAXGroupRole = "AXGroup"
public let kAXButtonRole = "AXButton"
public let kAXRadioButtonRole = "AXRadioButton" // New
public let kAXCheckBoxRole = "AXCheckBox"
public let kAXPopUpButtonRole = "AXPopUpButton" // New
public let kAXMenuButtonRole = "AXMenuButton" // New
public let kAXStaticTextRole = "AXStaticText"
public let kAXTextFieldRole = "AXTextField"
public let kAXTextAreaRole = "AXTextArea"
public let kAXScrollAreaRole = "AXScrollArea"
public let kAXScrollBarRole = "AXScrollBar" // New
public let kAXWebAreaRole = "AXWebArea"
public let kAXImageRole = "AXImage" // New
public let kAXListRole = "AXList" // New
public let kAXTableRole = "AXTable" // New
public let kAXOutlineRole = "AXOutline" // New
public let kAXColumnRole = "AXColumn" // New
public let kAXRowRole = "AXRow" // New
public let kAXToolbarRole = "AXToolbar"
public let kAXBusyIndicatorRole = "AXBusyIndicator" // New
public let kAXProgressIndicatorRole = "AXProgressIndicator" // New
public let kAXSliderRole = "AXSlider" // New
public let kAXIncrementorRole = "AXIncrementor" // New
public let kAXDisclosureTriangleRole = "AXDisclosureTriangle" // New
public let kAXMenuRole = "AXMenu" // New
public let kAXMenuItemRole = "AXMenuItem" // New
public let kAXSplitGroupRole = "AXSplitGroup" // New
public let kAXSplitterRole = "AXSplitter" // New
public let kAXColorWellRole = "AXColorWell" // New
public let kAXLinkRole = "AXLink" // New
public let kAXUnknownRole = "AXUnknown" // New
// Attributes for web content and tables/lists
public let kAXVisibleChildrenAttribute = "AXVisibleChildren"
public let kAXSelectedChildrenAttribute = "AXSelectedChildren"
public let kAXTabsAttribute = "AXTabs" // Often a kAXRadioGroup or kAXTabGroup role
public let kAXRowsAttribute = "AXRows"
public let kAXColumnsAttribute = "AXColumns"
public let kAXSelectedRowsAttribute = "AXSelectedRows" // New
public let kAXSelectedColumnsAttribute = "AXSelectedColumns" // New
public let kAXIndexAttribute = "AXIndex" // New (for rows/columns)
public let kAXDisclosingAttribute = "AXDisclosing" // New (for outlines)
// Custom or less standard attributes (verify usage and standard names)
public let kAXPathHintAttribute = "AXPathHint" // Our custom attribute for pathing
// String constant for "not available"
public let kAXNotAvailableString = "n/a"
// DOM specific attributes (these seem custom or web-specific, not standard Apple AX)
// Verify if these are actual attribute names exposed by web views or custom implementations.
public let kAXDOMIdentifierAttribute = "AXDOMIdentifier" // Example, might not be standard AX
public let kAXDOMClassListAttribute = "AXDOMClassList" // Example, might not be standard AX
public let kAXARIADOMResourceAttribute = "AXARIADOMResource" // Example
public let kAXARIADOMFunctionAttribute = "AXARIADOM-función" // Corrected identifier, kept original string value.
public let kAXARIADOMChildrenAttribute = "AXARIADOMChildren" // New
public let kAXDOMChildrenAttribute = "AXDOMChildren" // New
// New constants for missing attributes
public let kAXToolbarButtonAttribute = "AXToolbarButton"
public let kAXProxyAttribute = "AXProxy"
public let kAXSelectedCellsAttribute = "AXSelectedCells"
public let kAXHeaderAttribute = "AXHeader"
public let kAXHorizontalScrollBarAttribute = "AXHorizontalScrollBar"
public let kAXVerticalScrollBarAttribute = "AXVerticalScrollBar"
// Attributes used in child heuristic collection (often non-standard or specific)
public let kAXWebAreaChildrenAttribute = "AXWebAreaChildren"
public let kAXHTMLContentAttribute = "AXHTMLContent"
public let kAXApplicationNavigationAttribute = "AXApplicationNavigation"
public let kAXApplicationElementsAttribute = "AXApplicationElements"
public let kAXContentsAttribute = "AXContents"
public let kAXBodyAreaAttribute = "AXBodyArea"
public let kAXDocumentContentAttribute = "AXDocumentContent"
public let kAXWebPageContentAttribute = "AXWebPageContent"
public let kAXSplitGroupContentsAttribute = "AXSplitGroupContents"
public let kAXLayoutAreaChildrenAttribute = "AXLayoutAreaChildren"
public let kAXGroupChildrenAttribute = "AXGroupChildren"
// Helper function to convert AXError to a string
public func axErrorToString(_ error: AXError) -> String {
switch error {
case .success: return "success"
case .failure: return "failure"
case .apiDisabled: return "apiDisabled"
case .invalidUIElement: return "invalidUIElement"
case .invalidUIElementObserver: return "invalidUIElementObserver"
case .cannotComplete: return "cannotComplete"
case .attributeUnsupported: return "attributeUnsupported"
case .actionUnsupported: return "actionUnsupported"
case .notificationUnsupported: return "notificationUnsupported"
case .notImplemented: return "notImplemented"
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
case .notificationNotRegistered: return "notificationNotRegistered"
case .noValue: return "noValue"
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
case .notEnoughPrecision: return "notEnoughPrecision"
case .illegalArgument: return "illegalArgument"
@unknown default:
return "unknown AXError (code: \(error.rawValue))"
}
}
// MARK: - Custom Application/Computed Keys
public let focusedApplicationKey = "focused"
public let computedNameAttributeKey = "ComputedName"
public let isClickableAttributeKey = "IsClickable"
public let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
public let computedPathAttributeKey = "ComputedPath"

View File

@ -0,0 +1,89 @@
import Foundation
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
public struct AnyCodable: Codable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self.value = ()
} else if let bool = try? container.decode(Bool.self) {
self.value = bool
} else if let int = try? container.decode(Int.self) {
self.value = int
} else if let int32 = try? container.decode(Int32.self) {
self.value = int32
} else if let int64 = try? container.decode(Int64.self) {
self.value = int64
} else if let uint = try? container.decode(UInt.self) {
self.value = uint
} else if let uint32 = try? container.decode(UInt32.self) {
self.value = uint32
} else if let uint64 = try? container.decode(UInt64.self) {
self.value = uint64
} else if let double = try? container.decode(Double.self) {
self.value = double
} else if let float = try? container.decode(Float.self) {
self.value = float
} else if let string = try? container.decode(String.self) {
self.value = string
} else if let array = try? container.decode([AnyCodable].self) {
self.value = array.map { $0.value }
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
self.value = dictionary.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "AnyCodable value cannot be decoded"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int32 as Int32:
try container.encode(Int(int32))
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let double as Double:
try container.encode(double)
case let float as Float:
try container.encode(float)
case let string as String:
try container.encode(string)
case let array as [AnyCodable]:
try container.encode(array)
case let array as [Any?]:
try container.encode(array.map { AnyCodable($0) })
case let dictionary as [String: AnyCodable]:
try container.encode(dictionary)
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
let context = EncodingError.Context(
codingPath: container.codingPath,
debugDescription: "AnyCodable value cannot be encoded"
)
throw EncodingError.invalidValue(value, context)
}
}
}

View File

@ -18,82 +18,82 @@ public struct Attribute<T> {
}
// MARK: - General Element Attributes
public static var role: Attribute<String> { Attribute<String>(kAXRoleAttribute) }
public static var subrole: Attribute<String> { Attribute<String>(kAXSubroleAttribute) }
public static var roleDescription: Attribute<String> { Attribute<String>(kAXRoleDescriptionAttribute) }
public static var title: Attribute<String> { Attribute<String>(kAXTitleAttribute) }
public static var description: Attribute<String> { Attribute<String>(kAXDescriptionAttribute) }
public static var help: Attribute<String> { Attribute<String>(kAXHelpAttribute) }
public static var identifier: Attribute<String> { Attribute<String>(kAXIdentifierAttribute) }
public static var role: Attribute<String> { Attribute<String>(AXAttributeNames.kAXRoleAttribute) }
public static var subrole: Attribute<String> { Attribute<String>(AXAttributeNames.kAXSubroleAttribute) }
public static var roleDescription: Attribute<String> { Attribute<String>(AXAttributeNames.kAXRoleDescriptionAttribute) }
public static var title: Attribute<String> { Attribute<String>(AXAttributeNames.kAXTitleAttribute) }
public static var description: Attribute<String> { Attribute<String>(AXAttributeNames.kAXDescriptionAttribute) }
public static var help: Attribute<String> { Attribute<String>(AXAttributeNames.kAXHelpAttribute) }
public static var identifier: Attribute<String> { Attribute<String>(AXAttributeNames.kAXIdentifierAttribute) }
// MARK: - Value Attributes
// kAXValueAttribute can be many types. For a generic getter, Any might be appropriate,
// or specific versions if the context knows the type.
public static var value: Attribute<Any> { Attribute<Any>(kAXValueAttribute) }
public static var value: Attribute<Any> { Attribute<Any>(AXAttributeNames.kAXValueAttribute) }
// Example of a more specific value if known:
// static var stringValue: Attribute<String> { Attribute(kAXValueAttribute) }
// MARK: - State Attributes
public static var enabled: Attribute<Bool> { Attribute<Bool>(kAXEnabledAttribute) }
public static var focused: Attribute<Bool> { Attribute<Bool>(kAXFocusedAttribute) }
public static var busy: Attribute<Bool> { Attribute<Bool>(kAXElementBusyAttribute) }
public static var hidden: Attribute<Bool> { Attribute<Bool>(kAXHiddenAttribute) }
public static var enabled: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXEnabledAttribute) }
public static var focused: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXFocusedAttribute) }
public static var busy: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXElementBusyAttribute) }
public static var hidden: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXHiddenAttribute) }
// MARK: - Hierarchy Attributes
public static var parent: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXParentAttribute) }
public static var parent: Attribute<AXUIElement> { Attribute<AXUIElement>(AXAttributeNames.kAXParentAttribute) }
// For children, the direct attribute often returns [AXUIElement].
// Element.children getter then wraps these.
public static var children: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXChildrenAttribute) }
public static var children: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXChildrenAttribute) }
public static var selectedChildren: Attribute<[AXUIElement]> {
Attribute<[AXUIElement]>(kAXSelectedChildrenAttribute) }
public static var visibleChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleChildrenAttribute)
Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedChildrenAttribute) }
public static var visibleChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXVisibleChildrenAttribute)
}
public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXWindowsAttribute) }
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMainWindowAttribute)
public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXWindowsAttribute) }
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXMainWindowAttribute)
} // Can be nil
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedWindowAttribute)
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFocusedWindowAttribute)
} // Can be nil
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedUIElementAttribute)
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFocusedUIElementAttribute)
} // Can be nil
// MARK: - Application Specific Attributes
// public static var enhancedUserInterface: Attribute<Bool> { Attribute<Bool>(kAXEnhancedUserInterfaceAttribute) } // Constant not found, commenting out
public static var frontmost: Attribute<Bool> { Attribute<Bool>(kAXFrontmostAttribute) }
public static var mainMenu: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXMenuBarAttribute) }
public static var frontmost: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXFrontmostAttribute) }
public static var mainMenu: Attribute<AXUIElement> { Attribute<AXUIElement>(AXAttributeNames.kAXMenuBarAttribute) }
// public static var hiddenApplication: Attribute<Bool> { Attribute(kAXHiddenAttribute) } // Same as element hidden, but for app. Covered by .hidden
// MARK: - Window Specific Attributes
public static var minimized: Attribute<Bool> { Attribute<Bool>(kAXMinimizedAttribute) }
public static var modal: Attribute<Bool> { Attribute<Bool>(kAXModalAttribute) }
public static var defaultButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXDefaultButtonAttribute) }
public static var cancelButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCancelButtonAttribute) }
public static var closeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCloseButtonAttribute) }
public static var zoomButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXZoomButtonAttribute) }
public static var minimizeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMinimizeButtonAttribute) }
public static var toolbarButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXToolbarButtonAttribute) }
public static var fullScreenButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFullScreenButtonAttribute)
public static var minimized: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXMinimizedAttribute) }
public static var modal: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXModalAttribute) }
public static var defaultButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXDefaultButtonAttribute) }
public static var cancelButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXCancelButtonAttribute) }
public static var closeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXCloseButtonAttribute) }
public static var zoomButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXZoomButtonAttribute) }
public static var minimizeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXMinimizeButtonAttribute) }
public static var toolbarButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXToolbarButtonAttribute) }
public static var fullScreenButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFullScreenButtonAttribute)
}
public static var proxy: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXProxyAttribute) }
public static var growArea: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXGrowAreaAttribute) }
public static var proxy: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXProxyAttribute) }
public static var growArea: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXGrowAreaAttribute) }
// MARK: - Table/List/Outline Attributes
public static var rows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXRowsAttribute) }
public static var columns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXColumnsAttribute) }
public static var selectedRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedRowsAttribute) }
public static var selectedColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedColumnsAttribute)
public static var rows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXRowsAttribute) }
public static var columns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXColumnsAttribute) }
public static var selectedRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedRowsAttribute) }
public static var selectedColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedColumnsAttribute)
}
public static var selectedCells: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedCellsAttribute) }
public static var selectedCells: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedCellsAttribute) }
public static var visibleRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleRowsAttribute) }
public static var visibleColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleColumnsAttribute) }
public static var header: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXHeaderAttribute) }
public static var header: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXHeaderAttribute) }
public static var orientation: Attribute<String> { Attribute<String>(kAXOrientationAttribute)
} // e.g., kAXVerticalOrientationValue
// MARK: - Text Attributes
public static var selectedText: Attribute<String> { Attribute<String>(kAXSelectedTextAttribute) }
public static var selectedTextRange: Attribute<CFRange> { Attribute<CFRange>(kAXSelectedTextRangeAttribute) }
public static var numberOfCharacters: Attribute<Int> { Attribute<Int>(kAXNumberOfCharactersAttribute) }
public static var visibleCharacterRange: Attribute<CFRange> { Attribute<CFRange>(kAXVisibleCharacterRangeAttribute)
public static var selectedText: Attribute<String> { Attribute<String>(AXAttributeNames.kAXSelectedTextAttribute) }
public static var selectedTextRange: Attribute<CFRange> { Attribute<CFRange>(AXAttributeNames.kAXSelectedTextRangeAttribute) }
public static var numberOfCharacters: Attribute<Int> { Attribute<Int>(AXAttributeNames.kAXNumberOfCharactersAttribute) }
public static var visibleCharacterRange: Attribute<CFRange> { Attribute<CFRange>(AXAttributeNames.kAXVisibleCharacterRangeAttribute)
}
// Parameterized attributes are handled differently, often via functions.
// static var attributedStringForRange: Attribute<NSAttributedString> { Attribute(kAXAttributedStringForRangeParameterizedAttribute) }
@ -101,22 +101,22 @@ public struct Attribute<T> {
// MARK: - Scroll Area Attributes
public static var horizontalScrollBar: Attribute<AXUIElement?> {
Attribute<AXUIElement?>(kAXHorizontalScrollBarAttribute) }
Attribute<AXUIElement?>(AXAttributeNames.kAXHorizontalScrollBarAttribute) }
public static var verticalScrollBar: Attribute<AXUIElement?> {
Attribute<AXUIElement?>(kAXVerticalScrollBarAttribute) }
Attribute<AXUIElement?>(AXAttributeNames.kAXVerticalScrollBarAttribute) }
// MARK: - Action Related
// Action names are typically an array of strings.
public static var actionNames: Attribute<[String]> { Attribute<[String]>(kAXActionNamesAttribute) }
public static var actionNames: Attribute<[String]> { Attribute<[String]>(AXAttributeNames.kAXActionNamesAttribute) }
// Action description is parameterized by the action name, so a simple Attribute<String> isn't quite right.
// It would be kAXActionDescriptionAttribute, and you pass a parameter.
// For now, we will represent it as taking a string, and the usage site will need to handle parameterization.
public static var actionDescription: Attribute<String> { Attribute<String>(kAXActionDescriptionAttribute) }
public static var actionDescription: Attribute<String> { Attribute<String>(AXAttributeNames.kAXActionDescriptionAttribute) }
// MARK: - AXValue holding attributes (expect these to return AXValueRef)
// These will typically be unwrapped by a helper function (like ValueParser or similar) into their Swift types.
public static var position: Attribute<CGPoint> { Attribute<CGPoint>(kAXPositionAttribute) }
public static var size: Attribute<CGSize> { Attribute<CGSize>(kAXSizeAttribute) }
public static var position: Attribute<CGPoint> { Attribute<CGPoint>(AXAttributeNames.kAXPositionAttribute) }
public static var size: Attribute<CGSize> { Attribute<CGSize>(AXAttributeNames.kAXSizeAttribute) }
// Note: CGRect for kAXBoundsAttribute is also common if available.
// For now, relying on position and size.

View File

@ -0,0 +1,75 @@
// CommandModels.swift - Contains command-related model structs
import Foundation
// Main command envelope - REPLACED with definition from axorc.swift for consistency
public struct CommandEnvelope: Codable {
public let command_id: String
public let command: CommandType // Uses CommandType from this file
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debug_logging: Bool?
public let locator: Locator? // Locator from this file
public let path_hint: [String]?
public let max_elements: Int?
public let output_format: OutputFormat? // OutputFormat from this file
public let action_name: String? // For performAction
public let action_value: AnyCodable? // For performAction (AnyCodable from this file)
public let sub_commands: [CommandEnvelope]? // For batch command
// Added a public initializer for convenience, matching fields.
public init(command_id: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
payload: [String: String]? = nil,
debug_logging: Bool? = nil,
locator: Locator? = nil,
path_hint: [String]? = nil,
max_elements: Int? = nil,
output_format: OutputFormat? = nil,
action_name: String? = nil,
action_value: AnyCodable? = nil,
sub_commands: [CommandEnvelope]? = nil) {
self.command_id = command_id
self.command = command
self.application = application
self.attributes = attributes
self.payload = payload
self.debug_logging = debug_logging
self.locator = locator
self.path_hint = path_hint
self.max_elements = max_elements
self.output_format = output_format
self.action_name = action_name
self.action_value = action_value
self.sub_commands = sub_commands
}
}
// Locator for finding elements
public struct Locator: Codable {
public var match_all: Bool?
public var criteria: [String: String]
public var root_element_path_hint: [String]?
public var requireAction: String?
public var computed_name_contains: String?
enum CodingKeys: String, CodingKey {
case match_all
case criteria
case root_element_path_hint
case requireAction = "require_action"
case computed_name_contains
}
public init(match_all: Bool? = nil, criteria: [String: String] = [:], root_element_path_hint: [String]? = nil,
requireAction: String? = nil, computed_name_contains: String? = nil) {
self.match_all = match_all
self.criteria = criteria
self.root_element_path_hint = root_element_path_hint
self.requireAction = requireAction
self.computed_name_contains = computed_name_contains
}
}

View File

@ -0,0 +1,40 @@
// Models.swift - Contains core data models and type aliases
import Foundation
// Type alias for element attributes dictionary
public typealias ElementAttributes = [String: AnyCodable]
public struct AXElement: Codable {
public var attributes: ElementAttributes?
public var path: [String]?
public init(attributes: ElementAttributes?, path: [String]? = nil) {
self.attributes = attributes
self.path = path
}
}
// MARK: - Search Log Entry Model (for stderr JSON logging)
public struct SearchLogEntry: Codable {
public let d: Int // depth
public let eR: String? // elementRole
public let eT: String? // elementTitle
public let eI: String? // elementIdentifier
public let mD: Int // maxDepth
public let c: [String: String]? // criteria (abbreviated)
public let s: String // status (e.g., "vis", "found", "noMatch", "maxD")
public let iM: Bool? // isMatch (true, false, or nil if not applicable for this status)
// Public initializer
public init(d: Int, eR: String?, eT: String?, eI: String?, mD: Int, c: [String: String]?, s: String, iM: Bool?) {
self.d = d
self.eR = eR
self.eT = eT
self.eI = eI
self.mD = mD
self.c = c
self.s = s
self.iM = iM
}
}

View File

@ -0,0 +1,73 @@
// Element+Actions.swift - Action-related methods for Element
import ApplicationServices
import Foundation
// Action-related extension for Element
extension Element {
// MARK: - Actions
@MainActor
public func isActionSupported(_ actionName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Bool {
// dLog is not directly used here, logging comes from the attribute call
if let actions: [String] = attribute(
Attribute<[String]>.actionNames,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // This will respect the new dLog behavior in .attribute()
) {
return actions.contains(actionName)
}
return false
}
@MainActor
@discardableResult
public func performAction(
_ actionName: Attribute<String>,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) throws -> Element {
func dLog(_ message: String) {
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
let error = AXUIElementPerformAction(self.underlyingElement, actionName.rawValue as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog("Action \(actionName.rawValue) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName.rawValue) failed on element \(desc)", error)
}
return self
}
@MainActor
@discardableResult
public func performAction(_ actionName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) throws -> Element {
func dLog(_ message: String) {
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
let error = AXUIElementPerformAction(self.underlyingElement, actionName as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error)
}
return self
}
}

View File

@ -0,0 +1,85 @@
// Element+Description.swift - Extension for Element description functionality
import ApplicationServices // For AXUIElement and other C APIs
import Foundation
// MARK: - Element Description Extension
extension Element {
@MainActor
public func briefDescription(option: ValueFormatOption = .default, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
var descriptionParts: [String] = []
var tempLogs: [String] = []
if let role = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
descriptionParts.append("Role: \(role)")
}
// PID, Title, ID, DOMId for .default and .verbose
if option == .default || option == .verbose {
var pidLogs: [String] = []
if let pidValue = self.pid(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &pidLogs) {
descriptionParts.append("PID: \(pidValue)")
}
if isDebugLoggingEnabled && false { currentDebugLogs.append(contentsOf: pidLogs) }
if let title = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
if !title.isEmpty {
descriptionParts.append("Title: '\(title.truncated(to: 50))'")
}
}
if let id = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
if !id.isEmpty {
descriptionParts.append("ID: '\(id.truncated(to: 50))'")
}
}
if let domId = self.domIdentifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
if !domId.isEmpty {
descriptionParts.append("DOMId: '\(domId.truncated(to: 50))'")
}
}
}
// Value and Help for .verbose only
if option == .verbose {
if let value = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
let valueStr = String(describing: value)
if !valueStr.isEmpty && valueStr != "nil" {
descriptionParts.append("Value: '\(valueStr.truncated(to: 80))'")
}
}
if let help = self.help(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
if !help.isEmpty {
descriptionParts.append("Help: '\(help.truncated(to: 80))'")
}
}
}
// For .short, only Role is included (implicitly from the first lines)
if isDebugLoggingEnabled && false {
currentDebugLogs.append(contentsOf: tempLogs)
}
if descriptionParts.isEmpty {
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("briefDescription: No descriptive attributes found, falling back to underlyingElement description.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
return String(describing: self.underlyingElement)
}
// If .short and we have more than just Role (or Role+PID if PID was included for default), then shorten further.
// This logic might need refinement based on desired .short output.
if option == .short && descriptionParts.count > 1 {
if let role = self.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempLogs) { // Get role again without logging
return "Role: \(role)" // Just return role for short
} else {
return descriptionParts.first ?? String(describing: self.underlyingElement) // Fallback for short
}
}
return descriptionParts.joined(separator: ", ")
}
}

View File

@ -1,23 +1,42 @@
import ApplicationServices
import Foundation
// MARK: - Environment Variable Check for JSON Logging
// Copied from ElementSearch.swift - ideally this would be in a shared utility
private func getEnvVar(_ name: String) -> String? {
guard let value = getenv(name) else { return nil }
return String(cString: value)
}
private let AXORC_JSON_LOG_ENABLED: Bool = {
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
// Explicitly log the check to stderr for debugging the env var itself, specific to Element+Hierarchy.swift
fputs("[Element+Hierarchy.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
return envValue == "true"
}()
// MARK: - Element Hierarchy Logic
extension Element {
@MainActor
public func children(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } }
func dLog(_ message: String) {
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
let elementDescription = self.briefDescription(
let elementDescriptionForLog = self.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog("Getting children for element: \(elementDescription)")
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("Getting children for element: \(elementDescriptionForLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
var childCollector = ChildCollector()
// Collect children from various sources
collectDirectChildren(
collector: &childCollector,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -36,8 +55,15 @@ extension Element {
currentDebugLogs: &currentDebugLogs
)
let result = childCollector.finalizeResults(dLog: dLog)
dLog("Final children count from Element.children: \(result?.count ?? 0)")
let result = childCollector.finalizeResults(dLog: { message in
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
})
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("Final children count from Element.children: \(result?.count ?? 0)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
return result
}
@ -47,37 +73,45 @@ extension Element {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) {
// DO NOT CALL self.briefDescription() or self.attribute() BEFORE THE kAXChildrenAttribute CALL BELOW
// Log entry for this function can be done by the caller (Element.children) if needed before this is called,
// or log a generic message here without using self.briefDescription().
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren: Attempting to fetch kAXChildrenAttribute directly.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren: Attempting to fetch kAXChildrenAttribute directly.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
var value: CFTypeRef?
let error = AXUIElementCopyAttributeValue(self.underlyingElement, kAXChildrenAttribute as CFString, &value)
let error = AXUIElementCopyAttributeValue(self.underlyingElement, AXAttributeNames.kAXChildrenAttribute as CFString, &value)
// It's safer to get description AFTER the critical kAXChildrenAttribute call
let selfDescForLog = isDebugLoggingEnabled ? self.briefDescription(option: .short, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) : "Element(debug_off)"
let selfDescForLog = (isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED) ? self.briefDescription(option: .short, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) : "Element(json_log_on_or_debug_off)"
if error == .success {
if let childrenCFArray = value, CFGetTypeID(childrenCFArray) == CFArrayGetTypeID() {
if let directChildrenUI = childrenCFArray as? [AXUIElement] {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Successfully fetched and cast \(directChildrenUI.count) direct children.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Successfully fetched and cast \(directChildrenUI.count) direct children.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
collector.addChildren(from: directChildrenUI)
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was a CFArray but failed to cast to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was a CFArray but failed to cast to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
} else if let nonArrayValue = value {
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was not a CFArray. TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
} else if let nonArrayValue = value {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was not a CFArray. TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was nil despite .success error code.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was nil despite .success error code.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
} else if error == .noValue {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute has no value.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute has no value.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Error fetching kAXChildrenAttribute: \(error.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Error fetching kAXChildrenAttribute: \(error.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// No CFRelease(value) needed here if childrenCFArray was an array, as `as? [AXUIElement]` handles bridging.
// If it was nonArrayValue, it's a bit more ambiguous but usually these are also bridged or not needing manual release for simple gets.
}
@MainActor
@ -87,14 +121,16 @@ extension Element {
currentDebugLogs: inout [String]
) {
let alternativeAttributes: [String] = [
kAXVisibleChildrenAttribute, kAXWebAreaChildrenAttribute, kAXHTMLContentAttribute,
kAXARIADOMChildrenAttribute, kAXDOMChildrenAttribute, kAXApplicationNavigationAttribute,
kAXApplicationElementsAttribute, kAXContentsAttribute, kAXBodyAreaAttribute, kAXDocumentContentAttribute,
kAXWebPageContentAttribute, kAXSplitGroupContentsAttribute, kAXLayoutAreaChildrenAttribute,
kAXGroupChildrenAttribute, kAXSelectedChildrenAttribute, kAXRowsAttribute, kAXColumnsAttribute,
kAXTabsAttribute
AXAttributeNames.kAXVisibleChildrenAttribute, AXAttributeNames.kAXWebAreaChildrenAttribute, AXAttributeNames.kAXHTMLContentAttribute,
AXAttributeNames.kAXARIADOMChildrenAttribute, AXAttributeNames.kAXDOMChildrenAttribute, AXAttributeNames.kAXApplicationNavigationAttribute,
AXAttributeNames.kAXApplicationElementsAttribute, AXAttributeNames.kAXContentsAttribute, AXAttributeNames.kAXBodyAreaAttribute, AXAttributeNames.kAXDocumentContentAttribute,
AXAttributeNames.kAXWebPageContentAttribute, AXAttributeNames.kAXSplitGroupContentsAttribute, AXAttributeNames.kAXLayoutAreaChildrenAttribute,
AXAttributeNames.kAXGroupChildrenAttribute, AXAttributeNames.kAXSelectedChildrenAttribute, AXAttributeNames.kAXRowsAttribute, AXAttributeNames.kAXColumnsAttribute,
AXAttributeNames.kAXTabsAttribute
]
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectAlternativeChildren: Will iterate \(alternativeAttributes.count) alternative attributes.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectAlternativeChildren: Will iterate \(alternativeAttributes.count) alternative attributes.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
for attrName in alternativeAttributes {
collectChildrenFromAttribute(
@ -113,24 +149,36 @@ extension Element {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) {
var tempLogs: [String] = []
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Trying '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
var tempLogs: [String] = [] // attribute() method logs to this, and it respects AXORC_JSON_LOG_ENABLED internally
// This initial log for the function call itself needs to be conditional
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Trying '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
if let childrenUI: [AXUIElement] = attribute(
Attribute<[AXUIElement]>(attributeName),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
currentDebugLogs: &tempLogs // attribute() logs here conditionally
) {
currentDebugLogs.append(contentsOf: tempLogs)
// Append tempLogs to currentDebugLogs *only if* they would have been logged by attribute() anyway
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogs) }
if !childrenUI.isEmpty {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Successfully fetched \(childrenUI.count) children from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Successfully fetched \(childrenUI.count) children from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
collector.addChildren(from: childrenUI)
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Fetched EMPTY array from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Fetched EMPTY array from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
} else {
currentDebugLogs.append(contentsOf: tempLogs)
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Attribute '\(attributeName)' returned nil or was not [AXUIElement].", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogs) }
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Attribute '\(attributeName)' returned nil or was not [AXUIElement].", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
}
@ -142,26 +190,35 @@ extension Element {
) {
var tempLogsForRole: [String] = []
let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForRole)
currentDebugLogs.append(contentsOf: tempLogsForRole)
// Append role logs only if general debug logging is on and JSON is off
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForRole) }
if currentRole == kAXApplicationRole as String {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Element is AXApplication. Trying kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if currentRole == AXRoleNames.kAXApplicationRole as String {
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Element is AXApplication. Trying kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
var tempLogsForWindows: [String] = []
if let windowElementsUI: [AXUIElement] = attribute(
Attribute<[AXUIElement]>.windows,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForWindows
) {
currentDebugLogs.append(contentsOf: tempLogsForWindows)
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForWindows) }
if !windowElementsUI.isEmpty {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Successfully fetched \(windowElementsUI.count) windows.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Successfully fetched \(windowElementsUI.count) windows.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
collector.addChildren(from: windowElementsUI)
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Fetched EMPTY array from kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Fetched EMPTY array from kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
} else {
currentDebugLogs.append(contentsOf: tempLogsForWindows)
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Attribute kAXWindowsAttribute returned nil.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForWindows) }
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Attribute kAXWindowsAttribute returned nil.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
}
}
@ -179,14 +236,13 @@ private struct ChildCollector {
for childUI in childrenUI {
let childElement = Element(childUI)
if !uniqueChildrenSet.contains(childElement) {
// Log before adding
// AXorcist.formatDebugLogMessage("ChildCollector: Adding new child: \(childElement.briefDescription(option: .minimal))", ... ) - too verbose for now
collectedChildren.append(childElement)
uniqueChildrenSet.insert(childElement)
}
}
}
// dLog is now a closure passed in, which should itself be conditional
func finalizeResults(dLog: (String) -> Void) -> [Element]? {
if collectedChildren.isEmpty {
dLog("ChildCollector.finalizeResults: No children found for element after all collection methods.")

View File

@ -0,0 +1,80 @@
// Element+ParameterizedAttributes.swift - Extension for parameterized attribute functionality
import ApplicationServices // For AXUIElement and other C APIs
import Foundation
// MARK: - Parameterized Attributes Extension
extension Element {
@MainActor
public func parameterizedAttribute<T>(
_ attribute: Attribute<T>,
forParameter parameter: Any,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> T? {
func dLog(_ message: String) {
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
var cfParameter: CFTypeRef?
// Convert Swift parameter to CFTypeRef for the API
if var range = parameter as? CFRange {
cfParameter = AXValueCreate(.cfRange, &range)
} else if let string = parameter as? String {
cfParameter = string as CFString
} else if let number = parameter as? NSNumber {
cfParameter = number
} else if CFGetTypeID(parameter as CFTypeRef) != 0 { // Check if it's already a CFTypeRef-compatible type
cfParameter = (parameter as CFTypeRef)
} else {
dLog("parameterizedAttribute: Unsupported parameter type \(type(of: parameter))")
return nil
}
guard let actualCFParameter = cfParameter else {
dLog("parameterizedAttribute: Failed to convert parameter to CFTypeRef.")
return nil
}
var value: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(
underlyingElement,
attribute.rawValue as CFString,
actualCFParameter,
&value
)
if error != .success {
dLog("parameterizedAttribute: Error \(error.rawValue) getting attribute \(attribute.rawValue)")
return nil
}
guard let resultCFValue = value else { return nil }
// Use axValue's unwrapping and casting logic if possible, by temporarily creating an element and attribute
// This is a bit of a conceptual stretch, as axValue is designed for direct attributes.
// A more direct unwrap using ValueUnwrapper might be cleaner here.
let unwrappedValue = ValueUnwrapper.unwrap(
resultCFValue,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
guard let finalValue = unwrappedValue else { return nil }
// Perform type casting similar to axValue
if T.self == String.self {
if let str = finalValue as? String { return str as? T } else if let attrStr = finalValue as? NSAttributedString { return attrStr.string as? T }
return nil
}
if let castedValue = finalValue as? T {
return castedValue
}
dLog(
"parameterizedAttribute: Fallback cast attempt for attribute '\(attribute.rawValue)' to type \(T.self) FAILED. Unwrapped value was \(type(of: finalValue)): \(finalValue)"
)
return nil
}
}

View File

@ -0,0 +1,169 @@
import ApplicationServices
import Foundation
// Extension to generate a descriptive path string
extension Element {
@MainActor
// Update signature to include logging parameters
public func generatePathString(
upTo ancestor: Element? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> String {
func dLog(_ message: String) {
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0 // Safety break for very deep or circular hierarchies
let maxDepth = 25
var tempLogs: [String] = [] // Temporary logs for calls within the loop
dLog(
"generatePathString started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
)
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll() // Clear for each iteration
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs) // Append logs from briefDescription
if let ancestor = ancestor, element == ancestor {
dLog("generatePathString: Reached specified ancestor: \(briefDesc)")
break // Reached the specified ancestor
}
// Check role to prevent going above application or a window if its parent is the app
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentElement = element.parent(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentRole = parentElement?.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs)
if role == AXRoleNames.kAXApplicationRole ||
(role == AXRoleNames.kAXWindowRole && parentRole == AXRoleNames.kAXApplicationRole && ancestor == nil) {
dLog(
"generatePathString: Stopping at \(role == AXRoleNames.kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
)
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != AXRoleNames.kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathString: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathString: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let finalPath = pathComponents.reversed().joined(separator: " -> ")
dLog("generatePathString finished. Path: \(finalPath)")
return finalPath
}
// New function to return path components as an array
@MainActor
public func generatePathArray(
upTo ancestor: Element? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> [String] {
func dLog(_ message: String) { if isDebugLoggingEnabled && false { currentDebugLogs.append(message) } }
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0
let maxDepth = 25
var tempLogs: [String] = []
dLog(
"generatePathArray started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll()
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if let ancestor = ancestor, element == ancestor {
dLog("generatePathArray: Reached specified ancestor: \(briefDesc)")
break
}
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentElement = element.parent(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentRole = parentElement?.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if role == AXRoleNames.kAXApplicationRole ||
(role == AXRoleNames.kAXWindowRole && parentRole == AXRoleNames.kAXApplicationRole && ancestor == nil) {
dLog(
"generatePathArray: Stopping at \(role == AXRoleNames.kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
)
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != AXRoleNames.kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathArray: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathArray: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let reversedPathComponents = Array(pathComponents.reversed())
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
return reversedPathComponents
}
}

View File

@ -172,24 +172,14 @@ extension Element {
)
}
@MainActor
public func briefDescription(
option: ValueFormatOption = .default,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> String {
let role = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "Unknown"
let title = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let identifier = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
var description = role
if let title = title, !title.isEmpty {
description += " '\(title)'"
} else if let identifier = identifier, !identifier.isEmpty {
description += " (\(identifier))"
@MainActor public var domIdentifier: String? {
get {
var logs: [String] = [] // Logging for this specific getter, will be discarded if not used by caller
return attribute(Attribute<String>(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: false, currentDebugLogs: &logs)
}
return option == .verbose ? "<\(description)>" : description
}
@MainActor public func domIdentifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
return attribute(Attribute<String>(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
}

View File

@ -4,6 +4,20 @@ import ApplicationServices // For AXUIElement and other C APIs
import Foundation
// We might need to import ValueHelpers or other local modules later
// MARK: - Environment Variable Check for JSON Logging
// Copied from ElementSearch.swift - ideally this would be in a shared utility
private func getEnvVar(_ name: String) -> String? {
guard let value = getenv(name) else { return nil }
return String(cString: value)
}
private let AXORC_JSON_LOG_ENABLED: Bool = {
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
// Explicitly log the check to stderr for debugging the env var itself, specific to Element.swift
fputs("[Element.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
return envValue == "true"
}()
// Element struct is NOT @MainActor. Isolation is applied to members that need it.
public struct Element: Equatable, Hashable {
public let underlyingElement: AXUIElement
@ -26,7 +40,11 @@ public struct Element: Equatable, Hashable {
@MainActor
public func attribute<T>(_ attribute: Attribute<T>, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> T? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } }
func dLog(_ message: String) {
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
if T.self == [AXUIElement].self {
dLog("Element.attribute: Special handling for T == [AXUIElement]. Attribute: \(attribute.rawValue)")
@ -46,7 +64,7 @@ public struct Element: Equatable, Hashable {
dLog("Element.attribute: Value for '\(attribute.rawValue)' was nil despite .success.")
}
} else if error == .noValue {
dLog("Element.attribute: Attribute '\(attribute.rawValue)' has no value.")
dLog("Element.attribute: Attribute '\(attribute.rawValue)' has no value.")
} else {
dLog("Element.attribute: Error fetching '\(attribute.rawValue)': \(error.rawValue)")
}
@ -77,24 +95,22 @@ public struct Element: Equatable, Hashable {
}
} else if T.self == Bool.self {
if CFGetTypeID(unwrappedValue) == CFBooleanGetTypeID() {
// Reverted to as! based on new compiler note
let swiftBool = CFBooleanGetValue(unwrappedValue as! CFBoolean)
let swiftBool = CFBooleanGetValue((unwrappedValue as! CFBoolean))
return swiftBool as? T
}
} else if T.self == Int.self {
if CFGetTypeID(unwrappedValue) == CFNumberGetTypeID() {
if CFGetTypeID(unwrappedValue) == CFNumberGetTypeID() {
var intValue: Int = 0
// Reverted to as! based on new compiler note
if CFNumberGetValue(unwrappedValue as! CFNumber, .sInt64Type, &intValue) {
if CFNumberGetValue((unwrappedValue as! CFNumber), .sInt64Type, &intValue) {
return intValue as? T
}
}
} else if T.self == AXUIElement.self { // For single AXUIElement (e.g. parent)
if CFGetTypeID(unwrappedValue) == AXUIElementGetTypeID() {
if CFGetTypeID(unwrappedValue) == AXUIElementGetTypeID() {
return unwrappedValue as? T // Direct cast should work as it's already AXUIElement
}
}
} // Add other common types like NSNumber, AXValue (for CGPoint etc.) as needed
// If no specific conversion worked, try a direct cast (might work for Any or some CF-bridged types)
if let directCast = unwrappedValue as? T {
dLog("Element.attribute: Basic conversion succeeded with direct cast for T = \(String(describing: T.self)), Attribute: \(attribute.rawValue).")
@ -116,8 +132,8 @@ public struct Element: Equatable, Hashable {
currentDebugLogs: inout [String]
) -> CFTypeRef? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
if isDebugLoggingEnabled && false {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
var value: CFTypeRef?
@ -141,146 +157,12 @@ public struct Element: Equatable, Hashable {
return nil // Return nil if not success or if value was nil (though success should mean value is populated)
}
// MARK: - Common Attribute Getters (MOVED to Element+Properties.swift)
// MARK: - Status Properties (MOVED to Element+Properties.swift)
// MARK: - Hierarchy and Relationship Getters (Simpler ones MOVED to Element+Properties.swift)
// MARK: - Action-related (supportedActions MOVED to Element+Properties.swift)
// Remaining properties and methods will stay here for now
// (e.g., children, isActionSupported, performAction, parameterizedAttribute, briefDescription, generatePathString, static factories)
// (e.g., children, parameterizedAttribute, briefDescription, generatePathString, static factories)
// Action methods have been moved to Element+Actions.swift
// MOVED to Element+Hierarchy.swift
// @MainActor public var children: [Element]? { ... }
// MARK: - Actions (supportedActions moved, other action methods remain)
@MainActor
public func isActionSupported(_ actionName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Bool {
if let actions: [String] = attribute(
Attribute<[String]>.actionNames,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
return actions.contains(actionName)
}
return false
}
@MainActor
@discardableResult
public func performAction(
_ actionName: Attribute<String>,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) throws -> Element {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let error = AXUIElementPerformAction(self.underlyingElement, actionName.rawValue as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog("Action \(actionName.rawValue) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName.rawValue) failed on element \(desc)", error)
}
return self
}
@MainActor
@discardableResult
public func performAction(_ actionName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) throws -> Element {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let error = AXUIElementPerformAction(self.underlyingElement, actionName as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error)
}
return self
}
// MARK: - Parameterized Attributes
@MainActor
public func parameterizedAttribute<T>(
_ attribute: Attribute<T>,
forParameter parameter: Any,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> T? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var cfParameter: CFTypeRef?
// Convert Swift parameter to CFTypeRef for the API
if var range = parameter as? CFRange {
cfParameter = AXValueCreate(.cfRange, &range)
} else if let string = parameter as? String {
cfParameter = string as CFString
} else if let number = parameter as? NSNumber {
cfParameter = number
} else if CFGetTypeID(parameter as CFTypeRef) != 0 { // Check if it's already a CFTypeRef-compatible type
cfParameter = (parameter as CFTypeRef)
} else {
dLog("parameterizedAttribute: Unsupported parameter type \(type(of: parameter))")
return nil
}
guard let actualCFParameter = cfParameter else {
dLog("parameterizedAttribute: Failed to convert parameter to CFTypeRef.")
return nil
}
var value: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(
underlyingElement,
attribute.rawValue as CFString,
actualCFParameter,
&value
)
if error != .success {
dLog("parameterizedAttribute: Error \(error.rawValue) getting attribute \(attribute.rawValue)")
return nil
}
guard let resultCFValue = value else { return nil }
// Use axValue's unwrapping and casting logic if possible, by temporarily creating an element and attribute
// This is a bit of a conceptual stretch, as axValue is designed for direct attributes.
// A more direct unwrap using ValueUnwrapper might be cleaner here.
let unwrappedValue = ValueUnwrapper.unwrap(
resultCFValue,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
guard let finalValue = unwrappedValue else { return nil }
// Perform type casting similar to axValue
if T.self == String.self {
if let str = finalValue as? String { return str as? T }
else if let attrStr = finalValue as? NSAttributedString { return attrStr.string as? T }
return nil
}
if let castedValue = finalValue as? T {
return castedValue
}
dLog(
"parameterizedAttribute: Fallback cast attempt for attribute '\(attribute.rawValue)' to type \(T.self) FAILED. Unwrapped value was \(type(of: finalValue)): \(finalValue)"
)
return nil
}
// MOVED to Element+Hierarchy.swift
// @MainActor
// public func generatePathString() -> String { ... }
@ -299,28 +181,28 @@ public struct Element: Equatable, Hashable {
public func computedName(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
// Now uses the passed-in logging parameters for its internal calls
if let titleStr = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs),
!titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr }
!titleStr.isEmpty, titleStr != AXMiscConstants.kAXNotAvailableString { return titleStr }
if let valueStr: String = self.value(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) as? String, !valueStr.isEmpty, valueStr != kAXNotAvailableString { return valueStr }
) as? String, !valueStr.isEmpty, valueStr != AXMiscConstants.kAXNotAvailableString { return valueStr }
if let descStr = self.description(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr }
), !descStr.isEmpty, descStr != AXMiscConstants.kAXNotAvailableString { return descStr }
if let helpStr: String = self.attribute(
Attribute<String>(kAXHelpAttribute),
Attribute<String>(AXAttributeNames.kAXHelpAttribute),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr }
), !helpStr.isEmpty, helpStr != AXMiscConstants.kAXNotAvailableString { return helpStr }
if let phValueStr: String = self.attribute(
Attribute<String>(kAXPlaceholderValueAttribute),
Attribute<String>(AXAttributeNames.kAXPlaceholderValueAttribute),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
), !phValueStr.isEmpty, phValueStr != kAXNotAvailableString { return phValueStr }
), !phValueStr.isEmpty, phValueStr != AXMiscConstants.kAXNotAvailableString { return phValueStr }
let roleNameStr: String = self.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -330,7 +212,7 @@ public struct Element: Equatable, Hashable {
if let roleDescStr: String = self.roleDescription(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
), !roleDescStr.isEmpty, roleDescStr != kAXNotAvailableString {
), !roleDescStr.isEmpty, roleDescStr != AXMiscConstants.kAXNotAvailableString {
return "\(roleDescStr) (\(roleNameStr))"
}
return nil
@ -338,199 +220,3 @@ public struct Element: Equatable, Hashable {
// MARK: - Path and Hierarchy
}
// Convenience factory for the application element - already @MainActor
@MainActor
public func applicationElement(for bundleIdOrName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
// Now call pid() with logging parameters
guard let pid = pid(
forAppIdentifier: bundleIdOrName,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
// dLog for "Failed to find PID..." is now handled inside pid() itself or if it returns nil here, we can log the higher level failure.
// The message below is slightly redundant if pid() logs its own failure, but can be useful.
dLog("applicationElement: Failed to obtain PID for '\(bundleIdOrName)'. Check previous logs from pid().")
return nil
}
let appElement = AXUIElementCreateApplication(pid)
return Element(appElement)
}
// Convenience factory for the system-wide element - already @MainActor
@MainActor
public func systemWideElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element {
// This function doesn't do much logging itself, but consistent signature is good.
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Creating system-wide element.")
return Element(AXUIElementCreateSystemWide())
}
// Extension to generate a descriptive path string
extension Element {
@MainActor
// Update signature to include logging parameters
public func generatePathString(
upTo ancestor: Element? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> String {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0 // Safety break for very deep or circular hierarchies
let maxDepth = 25
var tempLogs: [String] = [] // Temporary logs for calls within the loop
dLog(
"generatePathString started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "nil")"
)
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll() // Clear for each iteration
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs) // Append logs from briefDescription
if let ancestor = ancestor, element == ancestor {
dLog("generatePathString: Reached specified ancestor: \(briefDesc)")
break // Reached the specified ancestor
}
// Check role to prevent going above application or a window if its parent is the app
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentElement = element.parent(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentRole = parentElement?.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs)
if role == kAXApplicationRole ||
(role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
dLog(
"generatePathString: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
)
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathString: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathString: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let finalPath = pathComponents.reversed().joined(separator: " -> ")
dLog("generatePathString finished. Path: \(finalPath)")
return finalPath
}
// New function to return path components as an array
@MainActor
public func generatePathArray(
upTo ancestor: Element? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> [String] {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0
let maxDepth = 25
var tempLogs: [String] = []
dLog(
"generatePathArray started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll()
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if let ancestor = ancestor, element == ancestor {
dLog("generatePathArray: Reached specified ancestor: \(briefDesc)")
break
}
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentElement = element.parent(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentRole = parentElement?.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if role == kAXApplicationRole ||
(role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
dLog(
"generatePathArray: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
)
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathArray: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathArray: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let reversedPathComponents = Array(pathComponents.reversed())
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
return reversedPathComponents
}
}

View File

@ -0,0 +1,37 @@
// ElementFactories.swift - Factory functions for creating Element instances
import ApplicationServices // For AXUIElement and other C APIs
import Foundation
// Convenience factory for the application element - already @MainActor
@MainActor
public func applicationElement(for bundleIdOrName: String, isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
// Now call pid() with logging parameters
guard let pid = pid(
forAppIdentifier: bundleIdOrName,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
// dLog for "Failed to find PID..." is now handled inside pid() itself or if it returns nil here, we can log the higher level failure.
// The message below is slightly redundant if pid() logs its own failure, but can be useful.
dLog("applicationElement: Failed to obtain PID for '\(bundleIdOrName)'. Check previous logs from pid().")
return nil
}
let appElement = AXUIElementCreateApplication(pid)
return Element(appElement)
}
// Convenience factory for the system-wide element - already @MainActor
@MainActor
public func systemWideElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element {
// This function doesn't do much logging itself, but consistent signature is good.
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Creating system-wide element.")
return Element(AXUIElementCreateSystemWide())
}

View File

@ -0,0 +1,25 @@
// ModelEnums.swift - Contains enum definitions for the AXorcist models
import Foundation
// Enum for output formatting options
public enum OutputFormat: String, Codable {
case smart // Default, tries to be concise and informative
case verbose // More detailed output, includes more attributes/info
case text_content // Primarily extracts textual content
case json_string // Returns the attributes as a JSON string (new)
}
// Define CommandType enum
public enum CommandType: String, Codable {
case query
case performAction = "performAction"
case getAttributes = "getAttributes"
case batch
case describeElement = "describeElement"
case getFocusedElement = "getFocusedElement"
case collectAll = "collectAll"
case extractText = "extractText"
case ping
// Add future commands here, ensuring case matches JSON or provide explicit raw value
}

View File

@ -1,376 +0,0 @@
// Models.swift - Contains Codable structs for command handling and responses
import Foundation
// Enum for output formatting options
public enum OutputFormat: String, Codable {
case smart // Default, tries to be concise and informative
case verbose // More detailed output, includes more attributes/info
case text_content // Primarily extracts textual content
case json_string // Returns the attributes as a JSON string (new)
}
// Define CommandType enum
public enum CommandType: String, Codable {
case query
case performAction = "performAction"
case getAttributes = "getAttributes"
case batch
case describeElement = "describeElement"
case getFocusedElement = "getFocusedElement"
case collectAll = "collectAll"
case extractText = "extractText"
case ping
// Add future commands here, ensuring case matches JSON or provide explicit raw value
}
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
public struct AnyCodable: Codable, @unchecked Sendable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self.value = ()
} else if let bool = try? container.decode(Bool.self) {
self.value = bool
} else if let int = try? container.decode(Int.self) {
self.value = int
} else if let int32 = try? container.decode(Int32.self) {
self.value = int32
} else if let int64 = try? container.decode(Int64.self) {
self.value = int64
} else if let uint = try? container.decode(UInt.self) {
self.value = uint
} else if let uint32 = try? container.decode(UInt32.self) {
self.value = uint32
} else if let uint64 = try? container.decode(UInt64.self) {
self.value = uint64
} else if let double = try? container.decode(Double.self) {
self.value = double
} else if let float = try? container.decode(Float.self) {
self.value = float
} else if let string = try? container.decode(String.self) {
self.value = string
} else if let array = try? container.decode([AnyCodable].self) {
self.value = array.map { $0.value }
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
self.value = dictionary.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "AnyCodable value cannot be decoded"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int32 as Int32:
try container.encode(Int(int32))
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let double as Double:
try container.encode(double)
case let float as Float:
try container.encode(float)
case let string as String:
try container.encode(string)
case let array as [AnyCodable]:
try container.encode(array)
case let array as [Any?]:
try container.encode(array.map { AnyCodable($0) })
case let dictionary as [String: AnyCodable]:
try container.encode(dictionary)
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
let context = EncodingError.Context(
codingPath: container.codingPath,
debugDescription: "AnyCodable value cannot be encoded"
)
throw EncodingError.invalidValue(value, context)
}
}
}
// Type alias for element attributes dictionary
public typealias ElementAttributes = [String: AnyCodable]
// Main command envelope - REPLACED with definition from axorc.swift for consistency
public struct CommandEnvelope: Codable {
public let command_id: String
public let command: CommandType // Uses CommandType from this file
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debug_logging: Bool?
public let locator: Locator? // Locator from this file
public let path_hint: [String]?
public let max_elements: Int?
public let output_format: OutputFormat? // OutputFormat from this file
public let action_name: String? // For performAction
public let action_value: AnyCodable? // For performAction (AnyCodable from this file)
public let sub_commands: [CommandEnvelope]? // For batch command
// Added a public initializer for convenience, matching fields.
public init(command_id: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
payload: [String: String]? = nil,
debug_logging: Bool? = nil,
locator: Locator? = nil,
path_hint: [String]? = nil,
max_elements: Int? = nil,
output_format: OutputFormat? = nil,
action_name: String? = nil,
action_value: AnyCodable? = nil,
sub_commands: [CommandEnvelope]? = nil) {
self.command_id = command_id
self.command = command
self.application = application
self.attributes = attributes
self.payload = payload
self.debug_logging = debug_logging
self.locator = locator
self.path_hint = path_hint
self.max_elements = max_elements
self.output_format = output_format
self.action_name = action_name
self.action_value = action_value
self.sub_commands = sub_commands
}
}
// Locator for finding elements
public struct Locator: Codable, Sendable {
public var match_all: Bool?
public var criteria: [String: String]
public var root_element_path_hint: [String]?
public var requireAction: String?
public var computed_name_contains: String?
enum CodingKeys: String, CodingKey {
case match_all
case criteria
case root_element_path_hint
case requireAction = "require_action"
case computed_name_contains
}
public init(match_all: Bool? = nil, criteria: [String: String] = [:], root_element_path_hint: [String]? = nil,
requireAction: String? = nil, computed_name_contains: String? = nil) {
self.match_all = match_all
self.criteria = criteria
self.root_element_path_hint = root_element_path_hint
self.requireAction = requireAction
self.computed_name_contains = computed_name_contains
}
}
// Response for query command (single element)
public struct QueryResponse: Codable {
public var command_id: String
public var success: Bool
public var command: String
public var data: AXElement?
public var attributes: ElementAttributes?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool = true, command: String = "getFocusedElement", data: AXElement? = nil,
attributes: ElementAttributes? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.command = command
self.data = data
self.attributes = attributes
self.error = error
self.debug_logs = debug_logs
}
// Custom init for HandlerResponse integration
public init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
self.command_id = command_id
self.success = success
self.command = command
self.data = handlerResponse.data
// If HandlerResponse has attributes, map them from its data field.
self.attributes = handlerResponse.data?.attributes
self.error = handlerResponse.error
self.debug_logs = debug_logs
}
}
// Response for collect_all command (multiple elements)
public struct MultiQueryResponse: Codable {
public var command_id: String
public var elements: [ElementAttributes]?
public var count: Int?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, elements: [ElementAttributes]? = nil, count: Int? = nil, error: String? = nil,
debug_logs: [String]? = nil) {
self.command_id = command_id
self.elements = elements
self.count = count ?? elements?.count
self.error = error
self.debug_logs = debug_logs
}
}
// Response for perform_action command
public struct PerformResponse: Codable {
public var command_id: String
public var success: Bool
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.error = error
self.debug_logs = debug_logs
}
}
// Response for extract_text command
public struct TextContentResponse: Codable {
public var command_id: String
public var text_content: String?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, text_content: String? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.text_content = text_content
self.error = error
self.debug_logs = debug_logs
}
}
// Generic error response
public struct ErrorResponse: Codable {
public var command_id: String
public var success: Bool
public var error: ErrorDetail
public var debug_logs: [String]?
public init(command_id: String, error: String, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = false
self.error = ErrorDetail(message: error)
self.debug_logs = debug_logs
}
}
public struct ErrorDetail: Codable {
public var message: String
public init(message: String) {
self.message = message
}
}
// Simple success response, e.g. for ping
public struct SimpleSuccessResponse: Codable, Equatable {
public var command_id: String
public var success: Bool
public var status: String
public var message: String
public var details: String?
public var debug_logs: [String]?
public init(command_id: String, status: String, message: String, details: String? = nil,
debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = true
self.status = status
self.message = message
self.details = details
self.debug_logs = debug_logs
}
}
// Placeholder for any additional models if needed
public struct AXElement: Codable, Sendable {
public var attributes: ElementAttributes?
public var path: [String]?
public init(attributes: ElementAttributes?, path: [String]? = nil) {
self.attributes = attributes
self.path = path
}
}
// Extension to add JSON encoding functionality to QueryResponse
extension QueryResponse {
public func jsonString() throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(self)
return String(data: data, encoding: .utf8) ?? ""
}
}
// MARK: - Handler Response Models
public struct HandlerResponse: Codable, Sendable {
public var data: AXElement?
public var error: String?
public var debug_logs: [String]?
public init(data: AXElement? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.data = data
self.error = error
self.debug_logs = debug_logs
}
}
// Structure for custom JSON output of handleCollectAll
internal struct CollectAllOutput: Encodable {
let command_id: String
let success: Bool
let command: String
let collected_elements: [AXElement]
let app_bundle_id: String?
let debug_logs: [String]?
}
// ADDED BatchResponse struct
public struct BatchResponse: Codable {
public var command_id: String
public var success: Bool
public var results: [HandlerResponse] // Array of HandlerResponses for each sub-command
public var error: String? // For an overall batch error, if any
public var debug_logs: [String]?
public init(command_id: String, success: Bool, results: [HandlerResponse], error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.results = results
self.error = error
self.debug_logs = debug_logs
}
}

View File

@ -12,4 +12,4 @@ public enum PathUtils {
}
return (attributeName: String(parts[0]), expectedValue: String(parts[1]))
}
}
}

View File

@ -0,0 +1,176 @@
// ResponseModels.swift - Contains response model structs for AXorcist commands
import Foundation
// Response for query command (single element)
public struct QueryResponse: Codable {
public var command_id: String
public var success: Bool
public var command: String
public var data: AXElement?
public var attributes: ElementAttributes?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool = true, command: String = "getFocusedElement", data: AXElement? = nil,
attributes: ElementAttributes? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.command = command
self.data = data
self.attributes = attributes
self.error = error
self.debug_logs = debug_logs
}
// Custom init for HandlerResponse integration
public init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
self.command_id = command_id
self.success = success
self.command = command
self.data = handlerResponse.data
// If HandlerResponse has attributes, map them from its data field.
self.attributes = handlerResponse.data?.attributes
self.error = handlerResponse.error
self.debug_logs = debug_logs
}
}
// Extension to add JSON encoding functionality to QueryResponse
extension QueryResponse {
public func jsonString() throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(self)
return String(data: data, encoding: .utf8) ?? ""
}
}
// Response for collect_all command (multiple elements)
public struct MultiQueryResponse: Codable {
public var command_id: String
public var elements: [ElementAttributes]?
public var count: Int?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, elements: [ElementAttributes]? = nil, count: Int? = nil, error: String? = nil,
debug_logs: [String]? = nil) {
self.command_id = command_id
self.elements = elements
self.count = count ?? elements?.count
self.error = error
self.debug_logs = debug_logs
}
}
// Response for perform_action command
public struct PerformResponse: Codable {
public var command_id: String
public var success: Bool
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.error = error
self.debug_logs = debug_logs
}
}
// Response for extract_text command
public struct TextContentResponse: Codable {
public var command_id: String
public var text_content: String?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, text_content: String? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.text_content = text_content
self.error = error
self.debug_logs = debug_logs
}
}
// Generic error response
public struct ErrorResponse: Codable {
public var command_id: String
public var success: Bool
public var error: ErrorDetail
public var debug_logs: [String]?
public init(command_id: String, error: String, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = false
self.error = ErrorDetail(message: error)
self.debug_logs = debug_logs
}
}
public struct ErrorDetail: Codable {
public var message: String
public init(message: String) {
self.message = message
}
}
// Simple success response, e.g. for ping
public struct SimpleSuccessResponse: Codable, Equatable {
public var command_id: String
public var success: Bool
public var status: String
public var message: String
public var details: String?
public var debug_logs: [String]?
public init(command_id: String, status: String, message: String, details: String? = nil,
debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = true
self.status = status
self.message = message
self.details = details
self.debug_logs = debug_logs
}
}
public struct HandlerResponse: Codable {
public var data: AXElement?
public var error: String?
public var debug_logs: [String]?
public init(data: AXElement? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.data = data
self.error = error
self.debug_logs = debug_logs
}
}
public struct BatchResponse: Codable {
public var command_id: String
public var success: Bool
public var results: [HandlerResponse] // Array of HandlerResponses for each sub-command
public var error: String? // For an overall batch error, if any
public var debug_logs: [String]?
public init(command_id: String, success: Bool, results: [HandlerResponse], error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.results = results
self.error = error
self.debug_logs = debug_logs
}
}
// Structure for custom JSON output of handleCollectAll
internal struct CollectAllOutput: Encodable {
let command_id: String
let success: Bool
let command: String
let collected_elements: [AXElement]
let app_bundle_id: String?
let debug_logs: [String]?
}

View File

@ -5,9 +5,103 @@ import ApplicationServices
import Darwin
import Foundation
// MARK: - Environment Variable Check for JSON Logging
// (Copied from other files - consider a shared utility)
private func getEnvVar(_ name: String) -> String? {
guard let value = getenv(name) else { return nil }
return String(cString: value)
}
private let HANDLER_AXORC_JSON_LOG_ENABLED: Bool = {
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
// No fputs here, assuming it's primarily for Swift module debugging
return envValue == "true"
}()
// MARK: - Action & Data Handlers Extension
extension AXorcist {
// MARK: - Private Helper Methods
private func executeStandardAccessibilityAction(
_ axActionName: CFString,
on targetElement: Element,
actionNameForLog: String,
currentDebugLogs: inout [String]
) -> AXError {
let axStatus = AXUIElementPerformAction(targetElement.underlyingElement, axActionName)
if axStatus != .success {
let errorMessage = "[AXorcist.handlePerformAction] Failed to perform \(actionNameForLog) action: \(axErrorToString(axStatus))"
currentDebugLogs.append(errorMessage)
}
return axStatus
}
private func executeSetAttributeValueAction(
attributeName: String,
value: AnyCodable?,
on targetElement: Element,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> (errorMessage: String?, axStatus: AXError) {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
if attributeName.hasPrefix("AX") {
let axStatus = AXUIElementPerformAction(targetElement.underlyingElement, attributeName as CFString)
if axStatus != .success {
let errorMessage = "[AXorcist.handlePerformAction] Failed to perform action '\(attributeName)': \(axErrorToString(axStatus))"
return (errorMessage, axStatus)
}
return (nil, axStatus)
} else {
guard let actionValue = value else {
let errorMessage = "[AXorcist.handlePerformAction] Attribute action '\(attributeName)' requires an action_value, but none was provided."
return (errorMessage, .invalidUIElement)
}
var cfValue: CFTypeRef?
switch actionValue.value {
case let stringValue as String:
cfValue = stringValue as CFString
case let boolValue as Bool:
cfValue = boolValue as CFBoolean
case let intValue as Int:
var number = intValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .intType, &number)
case let doubleValue as Double:
var number = doubleValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number)
default:
if CFGetTypeID(actionValue.value as AnyObject) != 0 {
cfValue = actionValue.value as AnyObject
dLog("[AXorcist.handlePerformAction] Warning: Attempting to use actionValue of type '\(type(of: actionValue.value))' directly as CFTypeRef for attribute '\(attributeName)'. This might not work as expected.")
} else {
let errorMessage = "[AXorcist.handlePerformAction] Unsupported value type '\(type(of: actionValue.value))' for attribute '\(attributeName)'. Cannot convert to CFTypeRef."
dLog(errorMessage)
return (errorMessage, .invalidUIElement)
}
}
guard let finalCFValue = cfValue else {
let errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(attributeName)' to a CoreFoundation type."
return (errorMessage, .invalidUIElement)
}
let axStatus = AXUIElementSetAttributeValue(targetElement.underlyingElement, attributeName as CFString, finalCFValue)
if axStatus != .success {
let errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(attributeName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
return (errorMessage, axStatus)
}
return (nil, axStatus)
}
}
@MainActor
public func handlePerformAction(
for appIdentifierOrNil: String? = nil,
@ -18,7 +112,7 @@ extension AXorcist {
maxDepth: Int? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
) async -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
@ -29,74 +123,26 @@ extension AXorcist {
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handlePerformAction] Handling for app: \(appIdentifier), action: \(actionName)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to get application element for identifier: \(appIdentifier)"
currentDebugLogs.append(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
let targetElementResult = await self.findTargetElement(
for: appIdentifierOrNil,
locator: locator,
pathHint: pathHint,
isRootedAtApp: true,
baseElement: nil,
maxDepthForSearch: maxDepth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let targetElement: Element
switch targetElementResult {
case .success(let element):
targetElement = element
case .failure(let error):
return HandlerResponse(data: nil, error: error.message, debug_logs: error.logs ?? currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("[AXorcist.handlePerformAction] Navigating with path_hint: \(pathHint.joined(separator: " -> ")) from root \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let lastLogBeforeDebug = currentDebugLogs.last
let error: String
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
error = "Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
error = "Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else {
error = "[AXorcist.handlePerformAction] Failed to navigate using path hint: \(pathHint.joined(separator: " -> "))"
}
if isDebugLoggingEnabled {
if let actualLastLog = lastLogBeforeDebug {
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(error)'. LastLog: '\(actualLastLog)'")
} else {
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(error)'")
}
}
currentDebugLogs.append(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
effectiveElement = navigatedElement
dLog("[AXorcist.handlePerformAction] Successfully navigated path_hint. New effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
}
let targetElementForAction: Element
if let actualLocator = locator {
dLog("[AXorcist.handlePerformAction] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) using locator criteria: \(actualLocator.criteria)")
let searchResult = search(
element: effectiveElement,
locator: actualLocator,
requireAction: actualLocator.requireAction,
depth: 0,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
fputs("HANDLER_RAW_STDERR_BEFORE_LOG_APPEND handlePerformAction: searchResult.logs.count = \(searchResult.logs.count), currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for performAction")
currentDebugLogs.append(contentsOf: searchResult.logs)
fputs("HANDLER_RAW_STDERR_AFTER_LOG_APPEND handlePerformAction: currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_HANDLER")
guard let foundElement = searchResult.foundElement else {
let error = "[AXorcist.handlePerformAction] Search failed. Could not find element matching locator criteria \(actualLocator.criteria) starting from element \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))."
if !currentDebugLogs.contains(error) {
currentDebugLogs.append(error)
}
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
targetElementForAction = foundElement
dLog("[AXorcist.handlePerformAction] Found element via locator: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
} else {
targetElementForAction = effectiveElement
dLog("[AXorcist.handlePerformAction] No locator provided. Using current effectiveElement as target: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
}
dLog("[AXorcist.handlePerformAction] Element for action: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
dLog("[AXorcist.handlePerformAction] Element for action: \(targetElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
if let actionValue = actionValue {
let valueDescription = String(describing: actionValue.value)
dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)' with value: \(valueDescription)")
@ -109,77 +155,75 @@ extension AXorcist {
switch actionName.lowercased() {
case "press":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPressAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXPressAction as CFString,
on: targetElement,
actionNameForLog: "press",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform press action: \(axErrorToString(axStatus))"
}
case "increment":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXIncrementAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXIncrementAction as CFString,
on: targetElement,
actionNameForLog: "increment",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform increment action: \(axErrorToString(axStatus))"
}
case "decrement":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXDecrementAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXDecrementAction as CFString,
on: targetElement,
actionNameForLog: "decrement",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform decrement action: \(axErrorToString(axStatus))"
}
case "showmenu":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXShowMenuAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXShowMenuAction as CFString,
on: targetElement,
actionNameForLog: "showmenu",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform showmenu action: \(axErrorToString(axStatus))"
}
case "pick":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPickAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXPickAction as CFString,
on: targetElement,
actionNameForLog: "pick",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform pick action: \(axErrorToString(axStatus))"
}
case "cancel":
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXCancelAction as CFString)
axStatus = self.executeStandardAccessibilityAction(
AXActionNames.kAXCancelAction as CFString,
on: targetElement,
actionNameForLog: "cancel",
currentDebugLogs: &currentDebugLogs
)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform cancel action: \(axErrorToString(axStatus))"
}
default:
if actionName.hasPrefix("AX") {
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, actionName as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform action '\(actionName)': \(axErrorToString(axStatus))"
}
} else {
if let actionValue = actionValue {
var cfValue: CFTypeRef?
switch actionValue.value {
case let stringValue as String:
cfValue = stringValue as CFString
case let boolValue as Bool:
cfValue = boolValue as CFBoolean
case let intValue as Int:
var number = intValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .intType, &number)
case let doubleValue as Double:
var number = doubleValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number)
default:
if CFGetTypeID(actionValue.value as AnyObject) != 0 {
cfValue = actionValue.value as AnyObject
dLog("[AXorcist.handlePerformAction] Warning: Attempting to use actionValue of type '\(type(of: actionValue.value))' directly as CFTypeRef for attribute '\(actionName)'. This might not work as expected.")
} else {
errorMessage = "[AXorcist.handlePerformAction] Unsupported value type '\(type(of: actionValue.value))' for attribute '\(actionName)'. Cannot convert to CFTypeRef."
dLog(errorMessage!)
}
}
if errorMessage == nil, let finalCFValue = cfValue {
axStatus = AXUIElementSetAttributeValue(targetElementForAction.underlyingElement, actionName as CFString, finalCFValue)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(actionName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
}
} else if errorMessage == nil {
errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type."
}
} else {
errorMessage = "[AXorcist.handlePerformAction] Attribute action '\(actionName)' requires an action_value, but none was provided."
}
}
let result = self.executeSetAttributeValueAction(
attributeName: actionName,
value: actionValue,
on: targetElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
errorMessage = result.errorMessage
axStatus = result.axStatus
}
if let currentErrorMessage = errorMessage {
@ -198,7 +242,7 @@ extension AXorcist {
pathHint: [String]? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
) async -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line))
@ -208,126 +252,81 @@ extension AXorcist {
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[handleExtractText] Starting text extraction for app: \(appIdentifier)")
guard let appElement = applicationElement(
for: appIdentifier,
let targetElementResult = await self.findTargetElement(
for: appIdentifierOrNil,
locator: locator,
pathHint: pathHint,
isRootedAtApp: true,
baseElement: nil,
maxDepthForSearch: nil,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "[handleExtractText] Failed to get application element for \(appIdentifier)"
currentDebugLogs.append(errorMessage)
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
)
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("[handleExtractText] Navigating to element using path hint: \(pathHint.joined(separator: " -> "))")
guard let navigatedElement = navigateToElement(
from: appElement,
pathHint: pathHint,
let targetElementForExtract: Element
let appElement: Element
switch targetElementResult {
case .success(let element):
targetElementForExtract = element
// We need the app element for path generation, so get it separately
guard let appEl = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let lastLogBeforeDebug = currentDebugLogs.last
let errorMessage: String
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
errorMessage = "[handleExtractText] Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
errorMessage = "[handleExtractText] Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else {
errorMessage = "[handleExtractText] Failed to navigate to element using path hint: \(pathHint.joined(separator: " -> "))"
}
if isDebugLoggingEnabled {
if let actualLastLog = lastLogBeforeDebug {
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(errorMessage)'. LastLog: '\(actualLastLog)'")
} else {
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(errorMessage)'")
}
}
let errorMessage = "[handleExtractText] Failed to get application element for path generation: \(appIdentifier)"
currentDebugLogs.append(errorMessage)
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
effectiveElement = navigatedElement
dLog("[handleExtractText] Successfully navigated path_hint. New effectiveElement for text extraction: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
appElement = appEl
case .failure(let error):
return HandlerResponse(data: nil, error: error.message, debug_logs: error.logs ?? currentDebugLogs)
}
let targetElementForExtract: Element
if let actualLocator = locator {
dLog("[handleExtractText] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) using locator criteria: \(actualLocator.criteria)")
let searchResult = search(
element: effectiveElement,
locator: actualLocator,
requireAction: nil,
depth: 0,
maxDepth: DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
fputs("HANDLER_RAW_STDERR_BEFORE_LOG_APPEND handleExtractText: searchResult.logs.count = \(searchResult.logs.count), currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for extractText")
currentDebugLogs.append(contentsOf: searchResult.logs)
fputs("HANDLER_RAW_STDERR_AFTER_LOG_APPEND handleExtractText: currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_EXTRACT_TEXT")
guard let foundElement = searchResult.foundElement else {
let errorMessage = "[handleExtractText] Target element not found for locator: \(actualLocator) starting from \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
if !currentDebugLogs.contains(errorMessage) {
currentDebugLogs.append(errorMessage)
}
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
targetElementForExtract = foundElement
dLog("[handleExtractText] Found element via locator for text extraction: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
} else {
targetElementForExtract = effectiveElement
dLog("[handleExtractText] No locator. Using effectiveElement from path_hint/app_root as target for text extraction: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
}
dLog("[handleExtractText] Target element found: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)), attempting to extract text")
var attributes: [String: AnyCodable] = [:]
var extractedAnyText = false
if let valueCF = targetElementForExtract.rawAttributeValue(named: kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if let valueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if CFGetTypeID(valueCF) == CFStringGetTypeID() {
let extractedValueText = valueCF as! String
if !extractedValueText.isEmpty {
attributes["extractedValue"] = AnyCodable(extractedValueText)
extractedAnyText = true
dLog("[handleExtractText] Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
dLog("[handleExtractText] Extracted text from AXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
} else {
dLog("[handleExtractText] kAXValueAttribute was empty or not a string.")
dLog("[handleExtractText] AXValueAttribute was empty or not a string.")
}
} else {
dLog("[handleExtractText] kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(valueCF))")
dLog("[handleExtractText] AXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(valueCF))")
}
} else {
dLog("[handleExtractText] kAXValueAttribute not found or nil.")
dLog("[handleExtractText] AXValueAttribute not found or nil.")
}
if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: kAXSelectedTextAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXSelectedTextAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if CFGetTypeID(selectedValueCF) == CFStringGetTypeID() {
let extractedSelectedText = selectedValueCF as! String
if !extractedSelectedText.isEmpty {
attributes["extractedSelectedText"] = AnyCodable(extractedSelectedText)
extractedAnyText = true
dLog("[handleExtractText] Extracted selected text from kAXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
dLog("[handleExtractText] Extracted selected text from AXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
} else {
dLog("[handleExtractText] kAXSelectedTextAttribute was empty or not a string.")
dLog("[handleExtractText] AXSelectedTextAttribute was empty or not a string.")
}
} else {
dLog("[handleExtractText] kAXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValueCF))")
dLog("[handleExtractText] AXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValueCF))")
}
} else {
dLog("[handleExtractText] kAXSelectedTextAttribute not found or nil.")
dLog("[handleExtractText] AXSelectedTextAttribute not found or nil.")
}
if !extractedAnyText {
dLog("[handleExtractText] No text could be extracted from kAXValue or kAXSelectedText for element.")
dLog("[handleExtractText] No text could be extracted from AXValue or AXSelectedText for element.")
}
let pathArray = targetElementForExtract.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElementToReturn = AXElement(attributes: attributes, path: pathArray)
let axElementToReturn = AXElement(attributes: attributes, path: pathArray)
return HandlerResponse(data: axElementToReturn, error: nil, debug_logs: currentDebugLogs)
}
}

View File

@ -13,7 +13,7 @@ extension AXorcist {
subCommands: [CommandEnvelope], // The array of sub-commands to process
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> [HandlerResponse] {
) async -> [HandlerResponse] {
// Local debug logging function
func dLog(_ message: String, subCommandID: String? = nil) {
if isDebugLoggingEnabled {
@ -56,7 +56,7 @@ extension AXorcist {
) // Keep debug_logs nil for specific error, main logs will have the dLog entry
break
}
subCommandResponse = self.handleGetAttributes(
subCommandResponse = await self.handleGetAttributes(
for: subCommandEnvelope.application,
locator: locator,
requestedAttributes: subCommandEnvelope.attributes,
@ -74,7 +74,7 @@ extension AXorcist {
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleQuery(
subCommandResponse = await self.handleQuery(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
@ -92,11 +92,12 @@ extension AXorcist {
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleDescribeElement(
subCommandResponse = await self.handleDescribeElement(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
maxDepth: subCommandEnvelope.max_elements,
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.output_format,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
@ -113,7 +114,7 @@ extension AXorcist {
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
guard let actionName = subCommandEnvelope.action_name else {
let errorMsg = "Action name missing for performAction in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
@ -124,7 +125,7 @@ extension AXorcist {
// If only path_hint is provided, locator will be nil, which is fine for handlePerformAction.
// If only locator is provided, pathHint will be nil or empty, also fine.
// If both, handlePerformAction can use pathHint as root_element_path_hint for the locator.
subCommandResponse = self.handlePerformAction(
subCommandResponse = await self.handlePerformAction(
for: subCommandEnvelope.application,
locator: subCommandEnvelope.locator, // Pass along, might be nil
pathHint: subCommandEnvelope.path_hint, // Pass along, might be nil or empty
@ -142,7 +143,7 @@ extension AXorcist {
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleExtractText(
subCommandResponse = await self.handleExtractText(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,

View File

@ -62,7 +62,7 @@ extension AXorcist {
"[AXorcist.handleCollectAll] Starting. App: \(appNameForLog), Locator: \(locatorDesc), PathHint: \(pathHintDesc), MaxDepth: \(maxDepthDesc)"
)
let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXorcist.defaultMaxDepthCollectAll
let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXMiscConstants.defaultMaxDepthCollectAll
let attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch
let effectiveOutputFormat = outputFormat ?? .smart
@ -125,16 +125,15 @@ extension AXorcist {
if let loc = locator {
dLog("Locator provided. Searching for element from current startElement: \(startElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) with locator criteria: \(String(describing: loc.criteria))")
let searchResultCollectAll = search(element: startElement,
locator: loc,
requireAction: loc.requireAction,
depth: 0,
maxDepth: Self.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled)
self.recursiveCallDebugLogs.append("HANDLER_DEBUG: searchResultCollectAll.logs.count = \(searchResultCollectAll.logs.count) before append for collectAll")
let searchResultCollectAll = self.search(element: startElement,
locator: loc,
requireAction: loc.requireAction,
depth: 0,
maxDepth: AXMiscConstants.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs)
self.recursiveCallDebugLogs.append(contentsOf: searchResultCollectAll.logs)
self.recursiveCallDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_COLLECT_ALL")
if let locatedStartElement = searchResultCollectAll.foundElement {
dLog("Locator found element: \(locatedStartElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)). This will be the root for collectAll recursion.")
@ -187,7 +186,7 @@ extension AXorcist {
var childrenRef: CFTypeRef?
let childrenResult = AXUIElementCopyAttributeValue(
axUIElement,
kAXChildrenAttribute as CFString,
AXAttributeNames.kAXChildrenAttribute as CFString,
&childrenRef
)
@ -215,7 +214,7 @@ extension AXorcist {
// Start recursion from the determined startElement
if !self.recursiveCallDebugLogs.contains(where: { $0.contains("Failed to find element with provided locator criteria") && $0.contains("Cannot start collectAll") }) {
// Only start if locator search (if any) didn't critically fail and try to return early.
// Only start if locator search (if any) didn't critically fail and try to return early.
collectRecursively(startElement.underlyingElement, 0)
}
@ -229,4 +228,4 @@ extension AXorcist {
)
return encode(output)
}
}
}

View File

@ -0,0 +1,145 @@
// AXorcist+HandlerUtils.swift - Common handler utilities
import AppKit
import ApplicationServices
import Foundation
// MARK: - Handler Error Types
internal struct HandlerResponseError: Error {
let message: String
let logs: [String]?
init(message: String, logs: [String]? = nil) {
self.message = message
self.logs = logs
}
}
// MARK: - Handler Utilities Extension
extension AXorcist {
/// Finds a target element using path hints and locator criteria
/// - Parameters:
/// - appIdentifierOrNil: Application identifier (nil uses focused app)
/// - locator: Optional locator criteria for finding the element
/// - pathHint: Optional path hint for navigation from root
/// - isRootedAtApp: If true, starts from application element; if false, uses baseElement
/// - baseElement: Base element to start from (only used if isRootedAtApp is false)
/// - maxDepthForSearch: Maximum search depth for locator searches
/// - isDebugLoggingEnabled: Whether debug logging is enabled
/// - currentDebugLogs: Debug logs array to append to
/// - Returns: Result containing the found Element or HandlerResponseError
@MainActor
internal func findTargetElement(
for appIdentifierOrNil: String?,
locator: Locator?,
pathHint: [String]?,
isRootedAtApp: Bool = true,
baseElement: Element? = nil,
maxDepthForSearch: Int? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) async -> Result<Element, HandlerResponseError> {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// Determine initial element
let initialElement: Element
if isRootedAtApp {
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[findTargetElement] Getting application element for: \(appIdentifier)")
guard let appElement = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "Failed to get application element for identifier: \(appIdentifier)"
dLog("[findTargetElement] \(errorMessage)")
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
}
initialElement = appElement
} else {
guard let providedBaseElement = baseElement else {
let errorMessage = "Base element required when isRootedAtApp is false, but none provided"
dLog("[findTargetElement] \(errorMessage)")
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
}
initialElement = providedBaseElement
}
var effectiveElement = initialElement
// Navigate using path hint if provided
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("[findTargetElement] Navigating with path_hint: \(pathHint.joined(separator: " -> ")) from root \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
guard let navigatedElement = navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let lastLogBeforeDebug = currentDebugLogs.last
let errorMessage: String
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
errorMessage = "Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
errorMessage = "Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
} else {
errorMessage = "Failed to navigate using path hint: \(pathHint.joined(separator: " -> "))"
}
if isDebugLoggingEnabled {
if let actualLastLog = lastLogBeforeDebug {
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(errorMessage)'. LastLog: '\(actualLastLog)'")
} else {
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(errorMessage)'")
}
}
dLog("[findTargetElement] \(errorMessage)")
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
}
effectiveElement = navigatedElement
dLog("[findTargetElement] Successfully navigated path_hint. New effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
}
// Search using locator if provided
if let actualLocator = locator {
dLog("[findTargetElement] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) using locator criteria: \(actualLocator.criteria)")
let searchResult = self.search(
element: effectiveElement,
locator: actualLocator,
requireAction: actualLocator.requireAction,
depth: 0,
maxDepth: maxDepthForSearch ?? AXMiscConstants.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
currentDebugLogs.append(contentsOf: searchResult.logs)
dLog("[findTargetElement] Search completed. Logs from searchResult.logs count: \(searchResult.logs.count)")
guard let foundElement = searchResult.foundElement else {
let errorMessage = "Search failed. Could not find element matching locator criteria \(actualLocator.criteria) starting from element \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
if !currentDebugLogs.contains(errorMessage) {
currentDebugLogs.append(errorMessage)
}
dLog("[findTargetElement] \(errorMessage)")
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
}
dLog("[findTargetElement] Found element via locator: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
return .success(foundElement)
} else {
// No locator, use effective element after path hint navigation
dLog("[findTargetElement] No locator provided. Using current effectiveElement as target: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
return .success(effectiveElement)
}
}
}

View File

@ -8,9 +8,9 @@ import Foundation
extension AXorcist {
// MARK: - handleQuery
@MainActor
internal func handleQuery(
public func handleQuery(
for appIdentifierOrNil: String?,
locator: Locator,
pathHint: [String]?,
@ -20,7 +20,7 @@ extension AXorcist {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) async -> HandlerResponse {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
@ -41,7 +41,7 @@ extension AXorcist {
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
if let navigatedElement = self.navigateToElement(
if let navigatedElement = navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -73,7 +73,7 @@ extension AXorcist {
dLog(
"Locator has root_element_path_hint: \(rootPathHint.joined(separator: " -> ")). Navigating from app element first."
)
guard let containerElement = self.navigateToElement(
guard let containerElement = navigateToElement(
from: appElement,
pathHint: rootPathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -94,18 +94,16 @@ extension AXorcist {
"Searching with locator from element (determined by main path_hint or app root): \(searchStartElementForLocator.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
)
}
let searchResult = search(
let searchResult = self.search(
element: searchStartElementForLocator,
locator: locator,
requireAction: locator.requireAction,
depth: 0,
maxDepth: maxDepth ?? AXorcist.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled
maxDepth: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for query")
currentDebugLogs.append(contentsOf: searchResult.logs)
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_QUERY")
foundElement = searchResult.foundElement
}
@ -114,7 +112,7 @@ extension AXorcist {
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
@ -122,7 +120,7 @@ extension AXorcist {
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let axElement = AXElement(attributes: attributes)
return HandlerResponse(
data: axElement,
@ -139,9 +137,9 @@ extension AXorcist {
}
// MARK: - handleGetAttributes
@MainActor
internal func handleGetAttributes(
public func handleGetAttributes(
for appIdentifierOrNil: String?,
locator: Locator,
requestedAttributes: [String]?,
@ -151,100 +149,63 @@ extension AXorcist {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) async -> HandlerResponse {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
dLog("Handling get_attributes command for app: \(appIdentifier)")
guard let appElement = applicationElement(
// Use findTargetElement to get the target element
let targetElementResult = await self.findTargetElement(
for: appIdentifier,
locator: locator,
pathHint: pathHint,
maxDepthForSearch: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("handleGetAttributes: \(errorMessage)")
)
let foundElement: Element
switch targetElementResult {
case .failure(let errorData):
return HandlerResponse(
data: nil,
error: errorMessage,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
error: errorData.message,
debug_logs: errorData.logs ?? currentDebugLogs
)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("handleGetAttributes: Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
if let navigatedElement = self.navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint: \(pathHint.joined(separator: " -> "))"
dLog("handleGetAttributes: \(errorMessage)")
return HandlerResponse(
data: nil,
error: errorMessage,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
)
}
case .success(let element):
foundElement = element
}
dLog(
"handleGetAttributes: Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
"handleGetAttributes: Element found: \(foundElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)). Fetching attributes: \(requestedAttributes ?? ["all"])..."
)
let searchResult = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
depth: 0,
maxDepth: maxDepth ?? AXorcist.defaultMaxDepthSearch,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for getAttributes")
currentDebugLogs.append(contentsOf: searchResult.logs)
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_GET_ATTRIBUTES")
let foundElement = searchResult.foundElement
if let elementToQuery = foundElement {
dLog(
"handleGetAttributes: Element found: \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)). Fetching attributes: \(requestedAttributes ?? ["all"])..."
)
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
dLog(
"Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))."
)
let axElement = AXElement(attributes: attributes)
return HandlerResponse(
data: axElement,
error: nil,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
)
} else {
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
dLog("handleGetAttributes: \(errorMessage)")
return HandlerResponse(
data: nil,
error: errorMessage,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
)
let elementToQuery = foundElement
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
dLog(
"Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))."
)
let axElement = AXElement(attributes: attributes)
return HandlerResponse(
data: axElement,
error: nil,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
)
}
@MainActor
public func handleDescribeElement(
for appIdentifierOrNil: String?,
@ -256,12 +217,41 @@ extension AXorcist {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) async -> HandlerResponse {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
dLog("Handling describe_element for app: \(appIdentifier)")
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
// Use findTargetElement to get the target element
let targetElementResult = await self.findTargetElement(
for: appIdentifier,
locator: locator,
pathHint: pathHint,
maxDepthForSearch: searchMaxDepth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let elementToDescribe: Element
switch targetElementResult {
case .failure(let errorData):
return HandlerResponse(
data: nil,
error: errorData.message,
debug_logs: errorData.logs ?? currentDebugLogs
)
case .success(let element):
elementToDescribe = element
}
dLog(
"[AXorcist.handleDescribeElement] Element found: \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)). Now describing."
)
// Get application element for path generation
guard let appElement = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -274,59 +264,11 @@ extension AXorcist {
)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("handleDescribeElement: Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
if let navigatedElement = self.navigateToElement(
from: appElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint for describe: \(pathHint.joined(separator: " -> "))"
dLog("handleDescribeElement: \(errorMessage)")
return HandlerResponse(
data: nil,
error: errorMessage,
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
)
}
}
dLog(
"[AXorcist.handleDescribeElement] Searching for element to describe using locator: \(locator.criteria) from effective element: \(effectiveElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
)
let searchMaxDepth = maxDepth ?? AXorcist.defaultMaxDepthSearch
let searchResult = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
depth: 0,
maxDepth: searchMaxDepth,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for describeElement")
currentDebugLogs.append(contentsOf: searchResult.logs)
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_DESCRIBE")
guard let elementToDescribe = searchResult.foundElement else {
let error = "[AXorcist.handleDescribeElement] Element to describe not found for locator: \(locator.criteria)"
currentDebugLogs.append(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
dLog(
"[AXorcist.handleDescribeElement] Element found: \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)). Now describing."
)
var attributes = getElementAttributes(
elementToDescribe,
requestedAttributes: requestedAttributes ?? ["all"],
forMultiDefault: true,
targetRole: locator.criteria[kAXRoleAttribute],
requestedAttributes: requestedAttributes ?? ["all"],
forMultiDefault: true,
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
outputFormat: outputFormat ?? .verbose,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
@ -339,7 +281,7 @@ extension AXorcist {
attributes: attributes,
path: elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
)
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
}
}
}

View File

@ -1,408 +0,0 @@
import AppKit
import ApplicationServices
import Foundation
// MARK: - Query Handler Methods Extension
extension AXorcist {
// Handle getting attributes for a specific element using locator
@MainActor
public func handleGetAttributes(
for appIdentifierOrNil: String? = nil,
locator: Locator,
requestedAttributes: [String]? = nil,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleGetAttributes] Handling for app: \(appIdentifier)")
guard let appElement = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
// Find element to get attributes from
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
let logMessage = "[AXorcist.handleGetAttributes] Navigating with path_hint: \(pathHintString)"
dLog(logMessage)
if let navigatedElement = navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
effectiveElement = navigatedElement
} else {
let pathHintStringForError = pathHint.joined(separator: " -> ")
_ = pathHintStringForError // Silences compiler warning
let errorMessageText = "Element not found via path hint: \(pathHintStringForError)"
dLog("[AXorcist.handleGetAttributes] \(errorMessageText)")
return HandlerResponse(data: nil, error: errorMessageText, debug_logs: currentDebugLogs)
}
}
var elementToQuery: Element?
let axApplicationKey = "AXApplication" // String literal for the attribute key
if locator.criteria.count == 1, let appCritervalue = locator.criteria[axApplicationKey],
(appCritervalue.uppercased() == "YES" || appCritervalue.uppercased() == "TRUE") {
let briefDesc = effectiveElement.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog(
"[AXorcist.handleGetAttributes] Locator criteria is {'\(axApplicationKey)': '\(appCritervalue)'}. Using effectiveElement (\(briefDesc)) as target."
)
elementToQuery = effectiveElement
} else {
let rootElementDescription = effectiveElement.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
dLog(
"[AXorcist.handleGetAttributes] Not an AXApplication-only locator or value mismatch. Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)"
)
let searchResultGetAttributes = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
currentDebugLogs.append(contentsOf: searchResultGetAttributes.logs)
elementToQuery = searchResultGetAttributes.foundElement
}
if let actualElementToQuery = elementToQuery {
let elementDescription = actualElementToQuery.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let attributesDescription = (requestedAttributes ?? ["all"]).description
dLog(
"[AXorcist.handleGetAttributes] Element identified/found: \(elementDescription). Fetching attributes: \(attributesDescription)..."
)
var attributes = getElementAttributes(
actualElementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = actualElementToQuery.generatePathArray(
upTo: appElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog(
"[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(actualElementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))."
)
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Handle query command - find an element matching criteria
@MainActor
public func handleQuery(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
requestedAttributes: [String]? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleQuery] Handling query for app: \(appIdentifier)")
guard let appElement = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
dLog("[AXorcist.handleQuery] Navigating with path_hint: \(pathHintString)")
if let navigatedElement = navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint: \(pathHintString)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Check if this is an app-only locator (only application/bundle_id/pid/path criteria)
let appSpecifiers = ["application", "bundle_id", "pid", "path"]
let criteriaKeys = locator.criteria.keys
let isAppOnlyLocator = criteriaKeys.allSatisfy { appSpecifiers.contains($0) } && criteriaKeys.count == 1
var foundElement: Element?
if isAppOnlyLocator {
dLog(
"[AXorcist.handleQuery] Locator is app-only (criteria: \(locator.criteria)). Using appElement directly."
)
foundElement = effectiveElement
} else {
dLog("[AXorcist.handleQuery] Locator contains element-specific criteria. Proceeding with search.")
var searchStartElementForLocator = appElement
if let rootPathHint = locator.root_element_path_hint, !rootPathHint.isEmpty {
let rootPathHintString = rootPathHint.joined(separator: " -> ")
_ = rootPathHintString // Silences compiler warning
dLog(
"[AXorcist.handleQuery] Locator has root_element_path_hint: \(rootPathHintString). Navigating from app element first."
)
guard let containerElement = navigateToElement(
from: appElement,
pathHint: rootPathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage =
"Container for locator not found via root_element_path_hint: \(rootPathHintString)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
searchStartElementForLocator = containerElement
let containerDescription = searchStartElementForLocator.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
_ = containerDescription // Silences compiler warning
dLog(
"[AXorcist.handleQuery] Searching with locator within container found by root_element_path_hint: \(containerDescription)"
)
} else {
searchStartElementForLocator = effectiveElement
let searchDescription = searchStartElementForLocator.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
_ = searchDescription // Silences compiler warning
dLog(
"[AXorcist.handleQuery] Searching with locator from element (determined by main path_hint or app root): \(searchDescription)"
)
}
let finalSearchTarget = (pathHint != nil && !pathHint!.isEmpty) ? effectiveElement :
searchStartElementForLocator
let searchResultQuery = search(
element: finalSearchTarget,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
currentDebugLogs.append(contentsOf: searchResultQuery.logs)
foundElement = searchResultQuery.foundElement
}
if let elementToQuery = foundElement {
let elementDescription = elementToQuery.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
_ = elementDescription // Silences compiler warning
dLog("[AXorcist.handleQuery] Element found: \(elementDescription). Fetching attributes...")
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToQuery.generatePathArray(
upTo: appElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleQuery] Successfully found and processed element with query.")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element matches query criteria with locator: \(String(describing: locator))"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Handle describe element command - provides comprehensive details about a specific element
@MainActor
public func handleDescribeElement(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleDescribeElement] Handling for app: \(appIdentifier)")
guard let appElement = applicationElement(
for: appIdentifier,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
dLog("[AXorcist.handleDescribeElement] Navigating with path_hint: \(pathHintString)")
if let navigatedElement = navigateToElement(
from: effectiveElement,
pathHint: pathHint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint for describe_element: \(pathHintString)"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
let rootElementDescription = effectiveElement.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
_ = rootElementDescription // Silences compiler warning
dLog(
"[AXorcist.handleDescribeElement] Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)"
)
let searchResultDescribe = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled
)
currentDebugLogs.append(contentsOf: searchResultDescribe.logs)
let foundElementForDescribe = searchResultDescribe.foundElement
if let elementToDescribe = foundElementForDescribe {
let elementDescription = elementToDescribe.briefDescription(
option: ValueFormatOption.default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
_ = elementDescription // Silences compiler warning
dLog(
"[AXorcist.handleDescribeElement] Element found: \(elementDescription). Describing with verbose output..."
)
// For describe_element, we typically want ALL attributes with verbose output
var attributes = getElementAttributes(
elementToDescribe,
requestedAttributes: [], // Empty means 'all standard' or 'all known'
forMultiDefault: true,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToDescribe.generatePathArray(
upTo: appElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog(
"[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))."
)
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element found for describe_element with locator: \(String(describing: locator))"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
}

View File

@ -0,0 +1,67 @@
// AttributeFormatter.swift - Contains functions for formatting element attributes for display
import ApplicationServices
import Foundation
// Note: This file assumes Element, ValueFormatOption, and AXMiscConstants.kAXNotAvailableString are available.
// formatAXValue is assumed to be available from elsewhere (e.g., ValueFormatter.swift)
// Helper function to format the parent attribute
@MainActor
internal func formatParentAttribute(_ parent: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let parentElement = parent else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(parentElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(parentElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
// Helper function to format the children attribute
@MainActor
internal func formatChildrenAttribute(_ children: [Element]?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualChildren = children, !actualChildren.isEmpty else { return AnyCodable("[]") }
if outputFormat == .text_content {
return AnyCodable("Array of \(actualChildren.count) Element(s)")
} else if outputFormat == .verbose {
var childrenSummaries: [String] = []
for childElement in actualChildren {
childrenSummaries.append(childElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
} else { // .smart output
return AnyCodable("Array of \(actualChildren.count) children")
}
}
// Helper function to format the focused UI element attribute
@MainActor
internal func formatFocusedUIElementAttribute(_ focusedElement: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualFocusedElement = focusedElement else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(actualFocusedElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(actualFocusedElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
// Helper function to format a raw CFTypeRef for .text_content output
@MainActor
internal func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) -> String {
guard let value = rawValue else { return AXMiscConstants.kAXNotAvailableString } // AXMiscConstants.kAXNotAvailableString needs to be defined
let typeID = CFGetTypeID(value)
if typeID == CFStringGetTypeID() { return (value as! String) } else if typeID == CFAttributedStringGetTypeID() { return (value as! NSAttributedString).string } else if typeID == AXValueGetTypeID() {
let axVal = value as! AXValue
// Assuming formatAXValue is globally available or accessible.
// If it's in a specific file like ValueFormatter.swift, direct call might need qualification
// or this function might need to be part of that file/extension.
// For now, assume it's callable like this.
return formatAXValue(axVal, option: .default) // Assumes formatAXValue returns String and ValueFormatOption.default is valid here
} else if typeID == CFNumberGetTypeID() { return (value as! NSNumber).stringValue } else if typeID == CFBooleanGetTypeID() { return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false" } else { return "<\(CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType")>" }
}

View File

@ -35,40 +35,40 @@ private func extractDirectPropertyValue(for attributeName: String, from element:
// Ensure logging parameters are passed to Element methods
switch attributeName {
case kAXPathHintAttribute:
extractedValue = element.attribute(Attribute<String>(kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXRoleAttribute:
case AXAttributeNames.kAXPathHintAttribute:
extractedValue = element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case AXAttributeNames.kAXRoleAttribute:
extractedValue = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXSubroleAttribute:
case AXAttributeNames.kAXSubroleAttribute:
extractedValue = element.subrole(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXTitleAttribute:
case AXAttributeNames.kAXTitleAttribute:
extractedValue = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXDescriptionAttribute:
case AXAttributeNames.kAXDescriptionAttribute:
extractedValue = element.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXEnabledAttribute:
case AXAttributeNames.kAXEnabledAttribute:
let val = element.isEnabled(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXFocusedAttribute:
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
case AXAttributeNames.kAXFocusedAttribute:
let val = element.isFocused(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXHiddenAttribute:
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
case AXAttributeNames.kAXHiddenAttribute:
let val = element.isHidden(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case isIgnoredAttributeKey:
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
case AXMiscConstants.isIgnoredAttributeKey:
let val = element.isIgnored(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val ? "true" : "false" }
case "PID":
let val = element.pid(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXElementBusyAttribute:
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
case AXAttributeNames.kAXElementBusyAttribute:
let val = element.isElementBusy(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
default:
handled = false
}
@ -81,9 +81,9 @@ private func determineAttributesToFetch(requestedAttributes: [String], forMultiD
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var attributesToFetch = requestedAttributes
if forMultiDefault {
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXTitleAttribute, kAXIdentifierAttribute]
if let role = targetRole, role == kAXStaticTextRole {
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXIdentifierAttribute]
attributesToFetch = [AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXValueAttribute, AXAttributeNames.kAXTitleAttribute, AXAttributeNames.kAXIdentifierAttribute]
if let role = targetRole, role == AXRoleNames.kAXStaticTextRole {
attributesToFetch = [AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXValueAttribute, AXAttributeNames.kAXIdentifierAttribute]
}
} else if attributesToFetch.isEmpty {
var attrNames: CFArray?
@ -115,19 +115,19 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
for attr in attributesToFetch {
var tempCallLogs: [String] = [] // Logs for a specific attribute fetching call
if attr == kAXParentAttribute {
if attr == AXAttributeNames.kAXParentAttribute {
tempCallLogs.removeAll()
let parent = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatParentAttribute will manage its own logs now
result[AXAttributeNames.kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatParentAttribute will manage its own logs now
currentDebugLogs.append(contentsOf: tempCallLogs) // Collect logs from element.parent and formatParentAttribute
continue
} else if attr == kAXChildrenAttribute {
} else if attr == AXAttributeNames.kAXChildrenAttribute {
tempCallLogs.removeAll()
let children = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[attr] = formatChildrenAttribute(children, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // Directly assign AnyCodable
currentDebugLogs.append(contentsOf: tempCallLogs)
continue
} else if attr == kAXFocusedUIElementAttribute {
} else if attr == AXAttributeNames.kAXFocusedUIElementAttribute {
tempCallLogs.removeAll()
let focused = element.focusedElement(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[attr] = formatFocusedUIElementAttribute(focused, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // Directly assign AnyCodable
@ -142,7 +142,6 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
if wasHandledDirectly {
finalValueToStore = directValue
dLog("Attribute '\(attr)' handled directly, value: \(String(describing: directValue))")
} else {
tempCallLogs.removeAll()
let rawCFValue: CFTypeRef? = element.rawAttributeValue(named: attr, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
@ -152,13 +151,11 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
} else {
finalValueToStore = formatCFTypeRef(rawCFValue, option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
dLog("Attribute '\(attr)' fetched via rawAttributeValue, formatted value: \(String(describing: finalValueToStore))")
}
if outputFormat == .smart {
if let strVal = finalValueToStore as? String,
strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == kAXNotAvailableString {
dLog("Smart format: Skipping attribute '\(attr)' with unhelpful value: \(strVal)")
strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == AXMiscConstants.kAXNotAvailableString {
continue
}
}
@ -166,36 +163,32 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
}
tempLogs.removeAll()
if result[computedNameAttributeKey] == nil {
if result[AXMiscConstants.computedNameAttributeKey] == nil {
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
result[computedNameAttributeKey] = AnyCodable(name)
dLog("Added ComputedName: \(name)")
result[AXMiscConstants.computedNameAttributeKey] = AnyCodable(name)
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if result[isClickableAttributeKey] == nil {
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if result[AXMiscConstants.isClickableAttributeKey] == nil {
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == AXRoleNames.kAXButtonRole)
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if isButton || hasPressAction {
result[isClickableAttributeKey] = AnyCodable(true)
dLog("Added IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
result[AXMiscConstants.isClickableAttributeKey] = AnyCodable(true)
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if outputFormat == .verbose && result[computedPathAttributeKey] == nil {
if outputFormat == .verbose && result[AXMiscConstants.computedPathAttributeKey] == nil {
let path = element.generatePathString(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
result[computedPathAttributeKey] = AnyCodable(path)
dLog("Added ComputedPath (verbose): \(path)")
result[AXMiscConstants.computedPathAttributeKey] = AnyCodable(path)
}
currentDebugLogs.append(contentsOf: tempLogs)
populateActionNamesAttribute(for: element, result: &result, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("getElementAttributes finished. Result keys: \(result.keys.joined(separator: ", "))")
return result
}
@ -203,8 +196,7 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
private func populateActionNamesAttribute(for element: Element, result: inout ElementAttributes, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if result[kAXActionNamesAttribute] != nil {
dLog("populateActionNamesAttribute: Already present or explicitly requested, skipping.")
if result[AXAttributeNames.kAXActionNamesAttribute] != nil {
return
}
currentDebugLogs.append(contentsOf: tempLogs) // Appending potentially empty tempLogs, for consistency, though it does nothing here.
@ -213,92 +205,38 @@ private func populateActionNamesAttribute(for element: Element, result: inout El
tempLogs.removeAll()
if let currentActions = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !currentActions.isEmpty {
actionsToStore = currentActions
dLog("populateActionNamesAttribute: Got \(currentActions.count) from supportedActions.")
} else {
dLog("populateActionNamesAttribute: supportedActions was nil or empty. Trying kAXActionsAttribute.")
tempLogs.removeAll() // Clear before next call that uses it
if let fallbackActions: [String] = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !fallbackActions.isEmpty {
if let fallbackActions: [String] = element.attribute(Attribute<[String]>(AXAttributeNames.kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !fallbackActions.isEmpty {
actionsToStore = fallbackActions
dLog("populateActionNamesAttribute: Got \(fallbackActions.count) from kAXActionsAttribute fallback.")
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let pressActionSupported = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
let pressActionSupported = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
dLog("populateActionNamesAttribute: kAXPressAction supported: \(pressActionSupported).")
if pressActionSupported {
if actionsToStore == nil { actionsToStore = [kAXPressAction] } else if !actionsToStore!.contains(kAXPressAction) { actionsToStore!.append(kAXPressAction) }
if actionsToStore == nil { actionsToStore = [AXActionNames.kAXPressAction] } else if !actionsToStore!.contains(AXActionNames.kAXPressAction) { actionsToStore!.append(AXActionNames.kAXPressAction) }
}
if let finalActions = actionsToStore, !finalActions.isEmpty {
result[kAXActionNamesAttribute] = AnyCodable(finalActions)
dLog("populateActionNamesAttribute: Final actions: \(finalActions.joined(separator: ", ")).")
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable(finalActions)
} else {
tempLogs.removeAll()
let primaryNil = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let fallbackNil = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
let fallbackNil = element.attribute(Attribute<[String]>(AXAttributeNames.kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
currentDebugLogs.append(contentsOf: tempLogs)
if primaryNil && fallbackNil && !pressActionSupported {
result[kAXActionNamesAttribute] = AnyCodable(kAXNotAvailableString)
dLog("populateActionNamesAttribute: All action sources nil/unsupported. Set to kAXNotAvailableString.")
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable(AXMiscConstants.kAXNotAvailableString)
} else {
result[kAXActionNamesAttribute] = AnyCodable("\(kAXNotAvailableString) (no specific actions found or list empty)")
dLog("populateActionNamesAttribute: Some action source present but list empty. Set to verbose kAXNotAvailableString.")
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable("\(AXMiscConstants.kAXNotAvailableString) (no specific actions found or list empty)")
}
}
}
// MARK: - Attribute Formatting Helpers
// Helper function to format the parent attribute
@MainActor
private func formatParentAttribute(_ parent: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let parentElement = parent else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(parentElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(parentElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
// Helper function to format the children attribute
@MainActor
private func formatChildrenAttribute(_ children: [Element]?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualChildren = children, !actualChildren.isEmpty else { return AnyCodable("[]") }
if outputFormat == .text_content {
return AnyCodable("Array of \(actualChildren.count) Element(s)")
} else if outputFormat == .verbose {
var childrenSummaries: [String] = []
for childElement in actualChildren {
childrenSummaries.append(childElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
} else { // .smart output
return AnyCodable("Array of \(actualChildren.count) children")
}
}
// Helper function to format the focused UI element attribute
@MainActor
private func formatFocusedUIElementAttribute(_ focusedElement: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualFocusedElement = focusedElement else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(actualFocusedElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(actualFocusedElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
/// Encodes the given ElementAttributes dictionary into a new dictionary containing
/// a single key "json_representation" with the JSON string as its value.
/// If encoding fails, returns a dictionary with an error message.
@ -328,26 +266,23 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
var attributes: ElementAttributes = [:]
tempLogs.removeAll()
dLog("getComputedAttributes for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))")
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
attributes[computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
dLog("ComputedName: \(name)")
attributes[AXMiscConstants.computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == AXRoleNames.kAXButtonRole)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from role call
tempLogs.removeAll()
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from isActionSupported call
if isButton || hasPressAction {
attributes[isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
dLog("IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
attributes[AXMiscConstants.isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
}
// Ensure other computed attributes like ComputedPath also use methods with logging if they exist.
@ -358,15 +293,5 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
// MARK: - Attribute Formatting Helpers (Additional)
// Helper function to format a raw CFTypeRef for .text_content output
@MainActor
private func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) -> String {
guard let value = rawValue else { return kAXNotAvailableString }
let typeID = CFGetTypeID(value)
if typeID == CFStringGetTypeID() { return (value as! String) } else if typeID == CFAttributedStringGetTypeID() { return (value as! NSAttributedString).string } else if typeID == AXValueGetTypeID() {
let axVal = value as! AXValue
return formatAXValue(axVal, option: .default) // Assumes formatAXValue returns String
} else if typeID == CFNumberGetTypeID() { return (value as! NSNumber).stringValue } else if typeID == CFBooleanGetTypeID() { return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false" } else { return "<\(CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType")>" }
}
// Any other attribute-specific helper functions could go here in the future.
// Formatting functions have been moved to AttributeFormatter.swift
// This includes: formatParentAttribute, formatChildrenAttribute, formatFocusedUIElementAttribute, formatRawCFValueForTextContent

View File

@ -22,8 +22,8 @@ internal func attributesMatch(
if !matchComputedNameAttributes(
element: element,
computedNameEquals: matchDetails[computedNameAttributeKey + "_equals"],
computedNameContains: matchDetails[computedNameAttributeKey + "_contains"],
computedNameEquals: matchDetails[AXMiscConstants.computedNameAttributeKey + "_equals"],
computedNameContains: matchDetails[AXMiscConstants.computedNameAttributeKey + "_contains"],
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
@ -32,12 +32,12 @@ internal func attributesMatch(
}
for (key, expectedValue) in matchDetails {
if key == computedNameAttributeKey + "_equals" || key == computedNameAttributeKey + "_contains" { continue }
if key == AXMiscConstants.computedNameAttributeKey + "_equals" || key == AXMiscConstants.computedNameAttributeKey + "_contains" { continue }
if key ==
kAXRoleAttribute { continue } // Already handled by ElementSearch's role check or not a primary filter here
AXAttributeNames.kAXRoleAttribute { continue } // Already handled by ElementSearch's role check or not a primary filter here
if key == kAXEnabledAttribute || key == kAXFocusedAttribute || key == kAXHiddenAttribute || key ==
kAXElementBusyAttribute || key == isIgnoredAttributeKey || key == kAXMainAttribute {
if key == AXAttributeNames.kAXEnabledAttribute || key == AXAttributeNames.kAXFocusedAttribute || key == AXAttributeNames.kAXHiddenAttribute || key ==
AXAttributeNames.kAXElementBusyAttribute || key == AXMiscConstants.isIgnoredAttributeKey || key == AXAttributeNames.kAXMainAttribute {
if !matchBooleanAttribute(
element: element,
key: key,
@ -51,7 +51,7 @@ internal func attributesMatch(
continue
}
if key == kAXActionNamesAttribute || key == kAXAllowedValuesAttribute || key == kAXChildrenAttribute {
if key == AXAttributeNames.kAXActionNamesAttribute || key == AXAttributeNames.kAXAllowedValuesAttribute || key == AXAttributeNames.kAXChildrenAttribute {
if !matchArrayAttribute(
element: element,
key: key,
@ -80,225 +80,3 @@ internal func attributesMatch(
dLog("attributesMatch [D\(depth)]: All attributes MATCHED criteria.")
return true
}
@MainActor
internal func matchStringAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if let currentValue = element.attribute(
Attribute<String>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
) {
if currentValue != expectedValueString {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' expected '\(expectedValueString)', but found '\(currentValue)'. No match."
)
return false
}
return true
} else {
if expectedValueString
.lowercased() == "nil" || expectedValueString == kAXNotAvailableString || expectedValueString.isEmpty {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' not found, but expected value ('\(expectedValueString)') suggests absence is OK. Match for this key."
)
return true
} else {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' (expected '\(expectedValueString)') not found or not convertible to String. No match."
)
return false
}
}
}
@MainActor
internal func matchArrayAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let expectedArray = decodeExpectedArray(
fromString: expectedValueString,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
dLog(
"matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match."
)
return false
}
var actualArray: [String]?
if key == kAXActionNamesAttribute {
actualArray = element.supportedActions(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
} else if key == kAXAllowedValuesAttribute {
actualArray = element.attribute(
Attribute<[String]>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
} else if key == kAXChildrenAttribute {
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?
.map { childElement -> String in
var childLogs: [String] = []
return childElement
.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
}
} else {
dLog(
"matchArrayAttribute [D\(depth)]: Unknown array key '\(key)'. This function needs to be extended for this key."
)
return false
}
if let actual = actualArray {
if Set(actual) != Set(expectedArray) {
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' expected '\(expectedArray)', but found '\(actual)'. Sets differ. No match."
)
return false
}
return true
} else {
if expectedArray.isEmpty {
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' not found, but expected array was empty. Match for this key."
)
return true
}
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
)
return false
}
}
@MainActor
internal func matchBooleanAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
var currentBoolValue: Bool?
switch key {
case kAXEnabledAttribute: currentBoolValue = element.isEnabled(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case kAXFocusedAttribute: currentBoolValue = element.isFocused(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case kAXHiddenAttribute: currentBoolValue = element.isHidden(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case kAXElementBusyAttribute: currentBoolValue = element.isElementBusy(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case isIgnoredAttributeKey: currentBoolValue = element.isIgnored(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case kAXMainAttribute: currentBoolValue = element.attribute(
Attribute<Bool>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
default:
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
return false
}
if let actualBool = currentBoolValue {
let expectedBool = expectedValueString.lowercased() == "true"
if actualBool != expectedBool {
dLog(
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' expected '\(expectedBool)', but found '\(actualBool)'. No match."
)
return false
}
return true
} else {
dLog(
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
)
return false
}
}
@MainActor
internal func matchComputedNameAttributes(
element: Element,
computedNameEquals: String?,
computedNameContains: String?,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if computedNameEquals == nil && computedNameContains == nil {
return true
}
// getComputedAttributes will need logging parameters
let computedAttrs = getComputedAttributes(
for: element,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
if let currentComputedNameAny = computedAttrs[computedNameAttributeKey]?.value,
// Assuming .value is how you get it from the AttributeData struct
let currentComputedName = currentComputedNameAny as? String {
if let equals = computedNameEquals {
if currentComputedName != equals {
dLog(
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' != '\(equals)'. No match."
)
return false
}
}
if let contains = computedNameContains {
if !currentComputedName.localizedCaseInsensitiveContains(contains) {
dLog(
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' does not contain '\(contains)'. No match."
)
return false
}
}
return true
} else {
dLog(
"matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none. No match."
)
return false
}
}

View File

@ -1,18 +1,181 @@
// ElementSearch.swift - Contains search and element collection logic
import ApplicationServices
import Darwin
import Foundation
// Variable DEBUG_LOGGING_ENABLED is expected to be globally available from Logging.swift
// Element is now the primary type for UI elements.
// MARK: - Environment Variable & Global Constants
// decodeExpectedArray MOVED to Utils/GeneralParsingUtils.swift
private func getEnvVar(_ name: String) -> String? {
guard let value = getenv(name) else { return nil }
return String(cString: value)
}
private let AXORC_JSON_LOG_ENABLED: Bool = {
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
fputs("[ElementSearch.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
return envValue == "true"
}()
// PathHintComponent and criteriaMatch are now in SearchCriteriaUtils.swift
// MARK: - Main Search Logic (findElementViaPathAndCriteria and its helpers)
@MainActor
func findElementViaPathAndCriteria(
application: Element,
locator: Locator,
maxDepth: Int?,
isDebugLoggingEnabledParam: Bool,
currentDebugLogs: inout [String]
) -> Element? {
var tempNilLogs: [String] = []
// ADDED DEBUG LOGGING
if isDebugLoggingEnabledParam {
let pathHintDebug = locator.root_element_path_hint?.joined(separator: " -> ") ?? "nil"
let initialMessage = "[findElementViaPathAndCriteria ENTRY] locator.criteria: \(locator.criteria), locator.root_element_path_hint: \(pathHintDebug)"
currentDebugLogs.append(AXorcist.formatDebugLogMessage(initialMessage, applicationName: application.pid(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).map { String($0) }, commandID: nil, file: #file, function: #function, line: #line))
}
// END ADDED DEBUG LOGGING
func dLog(_ message: String, depth: Int? = nil, status: String? = nil, element: Element? = nil, c: [String: String]? = nil, md: Int? = nil) {
if !AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabledParam {
var logMessage = message
if let depth_ = depth, let status_ = status, let element_ = element {
let role = element_.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs) ?? "nil"
let title = element_.title(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)?.truncated(to: 30) ?? "nil"
let id = element_.identifier(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)?.truncated(to: 30) ?? "nil"
let criteriaDesc = c?.description.truncated(to: 50) ?? locator.criteria.description.truncated(to: 50)
let maxDepthDesc = md ?? maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
logMessage = "search [D\(depth_)]: Path:\(element_.generatePathArray(upTo: application, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).suffix(3).joined(separator: "/")), Status:\(status_), Elem:\(role) T:'\(title)' ID:'\(id)', Crit:\(criteriaDesc), MaxD:\(maxDepthDesc)"
}
let appPidString = application.pid(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).map { String($0) }
currentDebugLogs.append(AXorcist.formatDebugLogMessage(logMessage, applicationName: appPidString, commandID: nil, file: #file, function: #function, line: #line))
}
}
func writeSearchLogEntry(depth: Int, element: Element?, criteriaForEntry: [String: String]?, maxDepthForEntry: Int, status: String, isMatch: Bool?) {
if AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabledParam {
let role: String? = element?.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
let title: String? = element?.title(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
let identifier: String? = element?.identifier(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
let entry = SearchLogEntry(
d: depth,
eR: role?.truncatedToMaxLogAbbrev(),
eT: title?.truncatedToMaxLogAbbrev(),
eI: identifier?.truncatedToMaxLogAbbrev(),
mD: maxDepthForEntry,
c: criteriaForEntry?.mapValues { $0.truncatedToMaxLogAbbrev() } ?? locator.criteria.mapValues { $0.truncatedToMaxLogAbbrev() },
s: status,
iM: isMatch
)
if let jsonData = try? JSONEncoder().encode(entry), let jsonString = String(data: jsonData, encoding: .utf8) {
fputs("\(jsonString)\n", stderr)
}
}
}
@MainActor
func navigateToElementByPathHint(pathHint: [PathHintComponent], initialSearchElement: Element, pathHintMaxDepth: Int) -> Element? {
var currentElementInPath = initialSearchElement
dLog("PathHintNav: Starting with \(pathHint.count) components from \(initialSearchElement.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs))")
for (index, pathComponent) in pathHint.enumerated() {
let currentNavigationDepth = index
dLog("PathHintNav: Visiting comp #\(index)", depth: currentNavigationDepth, status: "pathVis", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathVis", isMatch: nil)
if !pathComponent.matches(element: currentElementInPath, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: &currentDebugLogs) {
dLog("PathHintNav: No match for comp #\(index)", depth: currentNavigationDepth, status: "pathNoMatch", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathNoMatch", isMatch: false)
return nil
}
dLog("PathHintNav: Matched comp #\(index)", depth: currentNavigationDepth, status: "pathMatch", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathMatch", isMatch: true)
if index == pathHint.count - 1 {
return currentElementInPath
}
let nextPathComponentCriteria = pathHint[index + 1].criteria
var foundNextChild: Element?
if let children = currentElementInPath.children(isDebugLoggingEnabled: isDebugLoggingEnabledParam, currentDebugLogs: &currentDebugLogs) {
for child in children {
let tempPathComponent = PathHintComponent(criteria: nextPathComponentCriteria)
if tempPathComponent.matches(element: child, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: &currentDebugLogs) {
currentElementInPath = child
foundNextChild = child
break
}
}
}
if foundNextChild == nil {
dLog("PathHintNav: Could not find child for next comp #\(index + 1)", depth: currentNavigationDepth, status: "pathChildFail", element: currentElementInPath, c: nextPathComponentCriteria, md: pathHintMaxDepth)
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: nextPathComponentCriteria, maxDepthForEntry: pathHintMaxDepth, status: "pathChildFail", isMatch: false)
return nil
}
}
return currentElementInPath
}
@MainActor
func traverseAndSearch(currentElement: Element, currentDepth: Int, effectiveMaxDepth: Int) -> Element? {
dLog("Traverse: Visiting", depth: currentDepth, status: "vis", element: currentElement, md: effectiveMaxDepth)
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "vis", isMatch: nil)
if criteriaMatch(element: currentElement, criteria: locator.criteria, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: &currentDebugLogs) {
dLog("Traverse: Found", depth: currentDepth, status: "found", element: currentElement, md: effectiveMaxDepth)
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "found", isMatch: true)
return currentElement
} else {
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "noMatch", isMatch: false)
}
if currentDepth >= effectiveMaxDepth {
dLog("Traverse: MaxDepth", depth: currentDepth, status: "maxD", element: currentElement, md: effectiveMaxDepth)
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "maxD", isMatch: false)
return nil
}
if let children = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabledParam, currentDebugLogs: &currentDebugLogs) {
for child in children {
if let found = traverseAndSearch(currentElement: child, currentDepth: currentDepth + 1, effectiveMaxDepth: effectiveMaxDepth) {
return found
}
}
}
return nil
}
var searchStartElement = application
let resolvedMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
if let pathHintStrings = locator.root_element_path_hint, !pathHintStrings.isEmpty {
let pathHintComponents = pathHintStrings.compactMap { PathHintComponent(pathSegment: $0, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: &currentDebugLogs) }
if !pathHintComponents.isEmpty && pathHintComponents.count == pathHintStrings.count {
dLog("Starting path hint navigation. Number of components: \(pathHintComponents.count)")
if let elementFromPathHint = navigateToElementByPathHint(pathHint: pathHintComponents, initialSearchElement: application, pathHintMaxDepth: pathHintComponents.count - 1) {
dLog("Path hint navigation successful. New start: \(elementFromPathHint.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). Starting criteria search.")
searchStartElement = elementFromPathHint
} else {
dLog("Path hint navigation failed. Full search from app root.")
}
} else {
dLog("Path hint strings provided but failed to parse into components or some were invalid. Full search from app root.")
}
} else {
dLog("No path hint provided. Searching from application root.")
}
return traverseAndSearch(currentElement: searchStartElement, currentDepth: 0, effectiveMaxDepth: resolvedMaxDepth)
}
enum ElementMatchStatus {
case fullMatch // Role, attributes, and (if specified) action all match
case partialMatch_actionMissing // Role and attributes match, but a required action is missing
case noMatch // Role or attributes do not match
case fullMatch
case partialMatch_actionMissing
case noMatch
}
@MainActor
@ -24,71 +187,33 @@ internal func evaluateElementAgainstCriteria(
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> ElementMatchStatus {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
func el_dLog(_ message: String) {
if !AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabled { currentDebugLogs.append(message) }
}
var tempLogs: [String] = []
var tempLogs: [String] = [] // For calls to Element methods that need their own log scope temporarily
let currentElementRoleForLog: String? = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
let wantedRoleFromCriteria = locator.criteria[AXAttributeNames.kAXRoleAttribute]
let currentElementRoleForLog: String? = element.role(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
let wantedRoleFromCriteria = locator.criteria[kAXRoleAttribute]
var roleMatchesCriteria = false
if let currentRole = currentElementRoleForLog, let roleToMatch = wantedRoleFromCriteria, !roleToMatch.isEmpty,
roleToMatch != "*" {
if let currentRole = currentElementRoleForLog, let roleToMatch = wantedRoleFromCriteria, !roleToMatch.isEmpty, roleToMatch != "*" {
roleMatchesCriteria = (currentRole == roleToMatch)
} else {
roleMatchesCriteria = true // Wildcard/empty/nil role in criteria is a match
let wantedRoleStr = wantedRoleFromCriteria ?? "any"
let currentRoleStr = currentElementRoleForLog ?? "nil"
dLog(
"evaluateElementAgainstCriteria [D\(depth)]: Wildcard/empty/nil role in criteria ('\(wantedRoleStr)') considered a match for element role \(currentRoleStr)."
)
roleMatchesCriteria = true
}
if !roleMatchesCriteria {
dLog(
"evaluateElementAgainstCriteria [D\(depth)]: Role mismatch. Element role: \(currentElementRoleForLog ?? "nil"), Expected: \(wantedRoleFromCriteria ?? "any"). No match."
)
if !roleMatchesCriteria { return .noMatch }
if !criteriaMatch(element: element, criteria: locator.criteria, isDebugLoggingEnabled: isDebugLoggingEnabled, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: &currentDebugLogs) {
return .noMatch
}
// Role matches, now check other attributes
// attributesMatch will also need isDebugLoggingEnabled, currentDebugLogs
if !attributesMatch(
element: element,
matchDetails: locator.criteria,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
// attributesMatch itself will log the specific mismatch reason
dLog("evaluateElementAgainstCriteria [D\(depth)]: attributesMatch returned false. No match.")
return .noMatch
}
// Role and attributes match. Now check for required action.
if let requiredAction = actionToVerify, !requiredAction.isEmpty {
if !element.isActionSupported(
requiredAction,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
) {
dLog(
"evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched, but required action '\(requiredAction)' is MISSING."
)
let actionRequirement = actionToVerify ?? locator.requireAction
if let requiredAction = actionRequirement, !requiredAction.isEmpty {
if !element.isActionSupported(requiredAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
return .partialMatch_actionMissing
}
dLog(
"evaluateElementAgainstCriteria [D\(depth)]: Role, Attributes, and Required Action '\(requiredAction)' all MATCH."
)
} else {
dLog(
"evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched. No action to verify or action already included in locator.criteria for attributesMatch."
)
}
return .fullMatch
}
@ -97,113 +222,34 @@ public func search(element: Element,
locator: Locator,
requireAction: String?,
depth: Int = 0,
maxDepth: Int = DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: Bool
/* REMOVED: currentDebugLogs: inout [String] */) -> (foundElement: Element?, logs: [String]) { // CHANGED RETURN TYPE
fputs("SEARCH_FUNCTION_RAW_PRINT_STDERR: Depth \(depth), isDebug: \(isDebugLoggingEnabled)\n", stderr)
var internalSearchLogs: [String] = [] // NEW: Internal log storage
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
var tempLogs: [String] = []
if depth > maxDepth { return nil }
// DIRECT APPEND AND LOCAL LET FOR DEBUG FLAG
internalSearchLogs.append("SEARCH_ENTRY_DIRECT_APPEND: Depth \(depth), isDebugLoggingEnabledParam: \(isDebugLoggingEnabled)")
let localDebugEnabled = isDebugLoggingEnabled
internalSearchLogs.append("SEARCH_ENTRY_LOCALDEBUG: localDebugEnabled is \(localDebugEnabled)")
func dLog(_ message: String) { if localDebugEnabled { internalSearchLogs.append(message) } } // Appends to internalSearchLogs
var tempLogsForElementMethods: [String] = [] // For calls to Element methods that require inout logs
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
// Calls to element.role, element.title etc. will use tempLogsForElementMethods
// Their logs aren't the primary concern for *this* refactor's test, but they need a valid inout array.
let roleStr = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForElementMethods) ?? "nil"
let titleStr = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForElementMethods) ?? "N/A"
internalSearchLogs.append(contentsOf: tempLogsForElementMethods) // Append logs from element methods
tempLogsForElementMethods.removeAll() // Clear for next use
dLog(
"search [D\(depth)]: Visiting. Role: \(roleStr), Title: \(titleStr). Locator Criteria: [\(criteriaDesc)], Action: \(requireAction ?? "none")"
)
if depth > maxDepth {
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForElementMethods
)
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
tempLogsForElementMethods.removeAll()
dLog("search [D\(depth)]: Max depth \(maxDepth) reached for element \(briefDesc).")
return (nil, internalSearchLogs) // RETURN TUPLE
}
// evaluateElementAgainstCriteria still uses inout, its logs will be added to internalSearchLogs
var logsFromEvaluate: [String] = []
let matchStatus = evaluateElementAgainstCriteria(element: element,
locator: locator,
actionToVerify: requireAction,
actionToVerify: requireAction ?? locator.requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &logsFromEvaluate)
internalSearchLogs.append(contentsOf: logsFromEvaluate)
currentDebugLogs: &currentDebugLogs)
if matchStatus == .fullMatch {
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForElementMethods
)
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
tempLogsForElementMethods.removeAll()
dLog(
"search [D\(depth)]: evaluateElementAgainstCriteria returned .fullMatch for \(briefDesc). Returning element."
)
return (element, internalSearchLogs) // RETURN TUPLE
}
if matchStatus == .fullMatch { return element }
let briefDesc = element.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForElementMethods
)
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
tempLogsForElementMethods.removeAll()
if matchStatus == .partialMatch_actionMissing {
dLog(
"search [D\(depth)]: Element \(briefDesc) matched criteria but missed action '\(requireAction ?? "")'. Continuing child search."
)
}
if matchStatus == .noMatch {
dLog("search [D\(depth)]: Element \(briefDesc) did not match criteria. Continuing child search.")
}
let childrenToSearch: [Element] = element.children(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogsForElementMethods // Pass tempLogsForElementMethods here
) ?? []
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
tempLogsForElementMethods.removeAll()
if !childrenToSearch.isEmpty {
for childElement in childrenToSearch {
// RECURSIVE CALL
let recursiveResult = search( // No longer passes currentDebugLogs
element: childElement,
locator: locator,
requireAction: requireAction,
depth: depth + 1,
maxDepth: maxDepth,
isDebugLoggingEnabled: isDebugLoggingEnabled
// Removed: currentDebugLogs: &currentDebugLogs -> now &internalSearchLogs
)
internalSearchLogs.append(contentsOf: recursiveResult.logs) // Append logs from recursive call
if let found = recursiveResult.foundElement {
return (found, internalSearchLogs) // RETURN TUPLE
}
let childrenToSearch: [Element] = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
for childElement in childrenToSearch {
if let found = search(element: childElement,
locator: locator,
requireAction: requireAction,
depth: depth + 1,
maxDepth: maxDepth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) {
return found
}
}
return (nil, internalSearchLogs) // RETURN TUPLE
return nil
}
@MainActor
@ -218,79 +264,36 @@ public func collectAll(
elementsBeingProcessed: inout Set<Element>,
foundElements: inout [Element],
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String] // Added logging parameter
currentDebugLogs: inout [String]
) {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods
let briefDescCurrent = currentElement.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
if elementsBeingProcessed.contains(currentElement) || currentPath.contains(currentElement) {
dLog("collectAll [D\(depth)]: Cycle detected or element \(briefDescCurrent) already processed/in path.")
return
}
var tempLogs: [String] = []
if elementsBeingProcessed.contains(currentElement) || currentPath.contains(currentElement) { return }
elementsBeingProcessed.insert(currentElement)
if foundElements.count >= maxElements {
dLog(
"collectAll [D\(depth)]: Max elements limit of \(maxElements) reached before processing \(briefDescCurrent)."
)
if foundElements.count >= maxElements || depth > maxDepth {
elementsBeingProcessed.remove(currentElement)
return
}
if depth > maxDepth {
dLog("collectAll [D\(depth)]: Max depth \(maxDepth) reached for \(briefDescCurrent).")
elementsBeingProcessed.remove(currentElement)
return
}
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
dLog(
"collectAll [D\(depth)]: Visiting \(briefDescCurrent). Criteria: [\(criteriaDesc)], Action: \(locator.requireAction ?? "none")"
)
let matchStatus = evaluateElementAgainstCriteria(element: currentElement,
locator: locator,
actionToVerify: locator.requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
currentDebugLogs: &currentDebugLogs)
if matchStatus == .fullMatch {
if foundElements.count < maxElements {
if !foundElements.contains(currentElement) {
foundElements.append(currentElement)
dLog("collectAll [D\(depth)]: Added \(briefDescCurrent). Hits: \(foundElements.count)/\(maxElements)")
} else {
dLog(
"collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but already in foundElements."
)
}
} else {
dLog(
"collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but maxElements (\(maxElements)) already reached."
)
if !foundElements.contains(currentElement) {
foundElements.append(currentElement)
}
}
let childrenToExplore: [Element] = currentElement.children(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
) ?? []
let childrenToExplore: [Element] = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
elementsBeingProcessed.remove(currentElement)
let newPath = currentPath + [currentElement]
for child in childrenToExplore {
if foundElements.count >= maxElements {
dLog(
"collectAll [D\(depth)]: Max elements (\(maxElements)) reached during child traversal of \(briefDescCurrent). Stopping further exploration for this branch."
)
break
}
if foundElements.count >= maxElements { break }
collectAll(
appElement: appElement,
locator: locator,
@ -302,7 +305,16 @@ public func collectAll(
elementsBeingProcessed: &elementsBeingProcessed,
foundElements: &foundElements,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass through logs
currentDebugLogs: &currentDebugLogs
)
}
}
// Notes for compilation:
// 1. ValueUnwrapper.unwrap should be available.
// 2. AXorcist.formatDebugLogMessage should be available.
// 3. Element struct and its methods must be correctly defined.
// 4. Locator struct must be defined with `criteria: [String: String]`, `root_element_path_hint: [String]?`, and `requireAction: String?`.
// 5. AXAttributeNames.kAXRoleAttribute should be a defined constant (String).
// 6. ValueFormatOption enum (with .default, .short cases) must be available for Element.briefDescription.
// 7. SearchLogEntry struct is now in Models.swift

View File

@ -0,0 +1,147 @@
// PathNavigator.swift - Contains logic for navigating element hierarchies using path hints
import ApplicationServices
import Foundation
// Note: Assumes Element, PathUtils, Attribute, AXorcist.formatDebugLogMessage are available.
// Helper to check if the current element matches a specific attribute-value pair
@MainActor
internal func currentElementMatchesPathComponent(
_ element: Element,
attributeName: String,
expectedValue: String,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String] // For logging
) -> Bool {
if attributeName.isEmpty { // Should not happen if parsePathComponent is robust
return false
}
// Assuming Element.attribute can handle logging appropriately based on isDebugLoggingEnabled and AXORC_JSON_LOG_ENABLED
if let actualValue = element.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if actualValue == expectedValue {
return true
}
}
return false
}
// Updated navigateToElement to prioritize children
@MainActor
internal func navigateToElement(
from startElement: Element,
pathHint: [String],
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Element? {
func dLog(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
// Use the passed-in isDebugLoggingEnabled
if isDebugLoggingEnabled {
// Assumes AXorcist.formatDebugLogMessage is accessible, might need to be public or this moved to an AXorcist extension.
// For now, let it be, build will tell if it's an issue.
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: file, function: function, line: line))
}
}
var currentElement = startElement
var currentPathSegmentForLog = ""
for (index, pathComponentString) in pathHint.enumerated() {
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
// Element.briefDescription needs access to logging parameters
let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)")
let (attributeName, expectedValue) = PathUtils.parsePathComponent(pathComponentString)
guard !attributeName.isEmpty else {
dLog("CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty attribute name from pathComponentString '\(pathComponentString)'")
return nil
}
var foundMatchForThisComponent = false
var newElementForNextStep: Element?
// Priority 1: Check children using Element.children()
if let childrenFromElementDotChildren = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)")
for child in childrenFromElementDotChildren {
let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let actualValue = child.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog(" [Nav Child Check 1] Child: \(childBriefDescForLog), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
dLog("Matched child (from Element.children): \(childBriefDescForLog) for '\(attributeName):\(expectedValue)'")
newElementForNextStep = child
foundMatchForThisComponent = true
break
}
}
}
} else {
dLog("Current element \(briefDesc) has no children from Element.children() or children array was nil.")
}
// FALLBACK: If no child matched via Element.children(), try direct AXAttributeNames.kAXChildrenAttribute call (Heisenbug workaround)
if !foundMatchForThisComponent {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: No match from Element.children(). Trying direct AXAttributeNames.kAXChildrenAttribute fallback.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
var directChildrenValue: CFTypeRef?
let directChildrenError = AXUIElementCopyAttributeValue(currentElement.underlyingElement, AXAttributeNames.kAXChildrenAttribute as CFString, &directChildrenValue)
let currentElementDescForFallbackLog = isDebugLoggingEnabled ? currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) : "Element(debug_off)"
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Fallback is for element: \(currentElementDescForFallbackLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if directChildrenError == .success, let cfArray = directChildrenValue, CFGetTypeID(cfArray) == CFArrayGetTypeID() {
if let directAxElements = cfArray as? [AXUIElement] {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback found \(directAxElements.count) raw children for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
for axChild in directAxElements {
let childElement = Element(axChild)
let childBriefDescForLogFallback = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let actualValue = childElement.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog(" [Nav Child Check 2-Fallback] Child: \(childBriefDescForLogFallback), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Matched child (from direct fallback) for '\(attributeName):\(expectedValue)' on \(currentElementDescForFallbackLog). Child: \(childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
newElementForNextStep = childElement
foundMatchForThisComponent = true
break
}
}
}
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: CFArray failed to cast to [AXUIElement] for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
} else if directChildrenError != .success {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: Error fetching for \(currentElementDescForFallbackLog): \(directChildrenError.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: No children or not an array for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// Priority 2: If no child matched (even after fallback), check current element itself
if !foundMatchForThisComponent {
// Pass currentDebugLogs by reference to the global currentElementMatchesPathComponent
let matchResult = currentElementMatchesPathComponent(
currentElement,
attributeName: attributeName,
expectedValue: expectedValue,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass by ref
)
if matchResult {
dLog("Current element \(briefDesc) itself matches '\(attributeName):\(expectedValue)'. Retaining current element for this step.")
newElementForNextStep = currentElement
foundMatchForThisComponent = true
}
}
if foundMatchForThisComponent, let nextElement = newElementForNextStep {
currentElement = nextElement
} else {
dLog("Neither current element \(briefDesc) nor its children (after all checks) matched '\(attributeName):\(expectedValue)'. Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER")
return nil
}
}
dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
return currentElement
}

View File

@ -0,0 +1,89 @@
import ApplicationServices
import Foundation
// Note: This file assumes AXAttributeNames.kAXRoleAttribute is available from AXAttributeNameConstants.swift
// and ValueUnwrapper is available from its respective file.
// MARK: - PathHintComponent Definition
@MainActor
struct PathHintComponent {
let criteria: [String: String]
init?(pathSegment: String, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) {
var parsedCriteria: [String: String] = [:]
let pairs = pathSegment.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
for pair in pairs {
let keyValue = pair.split(separator: "=", maxSplits: 1).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
if keyValue.count == 2 {
parsedCriteria[String(keyValue[0])] = String(keyValue[1])
} else {
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent: Invalid key-value pair: \(pair)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
}
if parsedCriteria.isEmpty && !pathSegment.isEmpty {
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent: Path segment \"\(pathSegment)\" parsed into empty criteria.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
self.criteria = parsedCriteria
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent initialized with criteria: \(self.criteria) from segment: \(pathSegment)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// Convenience initializer if criteria is already a dictionary
init(criteria: [String: String]) {
self.criteria = criteria
}
func matches(element: Element, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
// Pass axorcJsonLogEnabled to criteriaMatch
return criteriaMatch(element: element, criteria: self.criteria, isDebugLoggingEnabled: isDebugLoggingEnabled, axorcJsonLogEnabled: axorcJsonLogEnabled, currentDebugLogs: &currentDebugLogs)
}
}
// MARK: - Criteria Matching Helper
@MainActor
func criteriaMatch(element: Element, criteria: [String: String]?, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
guard let criteria = criteria, !criteria.isEmpty else {
return true // No criteria means an automatic match
}
func cLog(_ message: String) {
// Use the passed-in axorcJsonLogEnabled parameter
if !axorcJsonLogEnabled && isDebugLoggingEnabled {
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
var tempNilLogs: [String] = [] // For briefDescription calls that don't need to pollute main logs
for (key, expectedValue) in criteria {
// Handle wildcard for role if specified
if key == AXAttributeNames.kAXRoleAttribute && expectedValue == "*" { continue }
var attributeValueCFType: CFTypeRef?
// Directly use underlyingElement for AX API calls
let error = AXUIElementCopyAttributeValue(element.underlyingElement, key as CFString, &attributeValueCFType)
guard error == .success, let actualValueCF = attributeValueCFType else {
cLog("Attribute \(key) not found or error \(error.rawValue) on element \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). No match.")
return false
}
// Use ValueUnwrapper to convert CFTypeRef to a Swift type
// Assuming ValueUnwrapper.unwrap is available and correctly handles logging parameters
let actualValueSwift: Any? = ValueUnwrapper.unwrap(actualValueCF, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let actualValueString = String(describing: actualValueSwift ?? "nil_after_unwrap")
// Perform case-insensitive comparison or exact match
if !(actualValueString.localizedCaseInsensitiveContains(expectedValue) || actualValueString == expectedValue) {
cLog("Attribute '\(key)' mismatch: Expected '\(expectedValue)', Got '\(actualValueString)'. Element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). No match.")
return false
}
cLog("Attribute '\(key)' matched: Expected '\(expectedValue)', Got '\(actualValueString)'.")
}
cLog("All criteria matched for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)).")
return true
}

View File

@ -1,9 +1,9 @@
// PathUtils.swift - Utilities for parsing paths and navigating element hierarchies.
import ApplicationServices // For Element, AXUIElement and kAX...Attribute constants
import ApplicationServices // For Element, AXUIElement and AX...Attribute constants
import Foundation
// Assumes Element is defined (likely via AXSwift an extension or typealias)
// debug() is assumed to be globally available from Logging.swift
// axValue<T>() is assumed to be globally available from ValueHelpers.swift
// kAXWindowRole, kAXWindowsAttribute, kAXChildrenAttribute, kAXRoleAttribute from AccessibilityConstants.swift
// AXRoleNames.kAXWindowRole, AXAttributeNames.kAXWindowsAttribute, AXAttributeNames.kAXChildrenAttribute, AXAttributeNames.kAXRoleAttribute from namespaced constants

View File

@ -0,0 +1,237 @@
// SpecificAttributeMatchers.swift - Contains specific helper functions for attribute matching.
import ApplicationServices
import Foundation
// Assumes Element, Attribute, AXMiscConstants.computedNameAttributeKey, AXMiscConstants.kAXNotAvailableString,
// AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXEnabledAttribute, AXAttributeNames.kAXFocusedAttribute, AXAttributeNames.kAXHiddenAttribute,
// AXAttributeNames.kAXElementBusyAttribute, AXMiscConstants.isIgnoredAttributeKey, AXAttributeNames.kAXMainAttribute,
// AXAttributeNames.kAXActionNamesAttribute, AXAttributeNames.kAXAllowedValuesAttribute, AXAttributeNames.kAXChildrenAttribute are available.
// Assumes decodeExpectedArray (from ValueParser or similar) and getComputedAttributes (from AttributeHelpers) are available.
@MainActor
internal func matchStringAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if let currentValue = element.attribute(
Attribute<String>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
) {
if currentValue != expectedValueString {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' expected '\(expectedValueString)', but found '\(currentValue)'. No match."
)
return false
}
return true
} else {
if expectedValueString
.lowercased() == "nil" || expectedValueString == AXMiscConstants.kAXNotAvailableString || expectedValueString.isEmpty {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' not found, but expected value ('\(expectedValueString)') suggests absence is OK. Match for this key."
)
return true
} else {
dLog(
"attributesMatch [D\(depth)]: Attribute '\(key)' (expected '\(expectedValueString)') not found or not convertible to String. No match."
)
return false
}
}
}
@MainActor
internal func matchArrayAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let expectedArray = decodeExpectedArray(
fromString: expectedValueString,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) else {
dLog(
"matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match."
)
return false
}
var actualArray: [String]?
if key == AXAttributeNames.kAXActionNamesAttribute {
actualArray = element.supportedActions(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
} else if key == AXAttributeNames.kAXAllowedValuesAttribute {
actualArray = element.attribute(
Attribute<[String]>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
} else if key == AXAttributeNames.kAXChildrenAttribute {
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?
.map { childElement -> String in
var childLogs: [String] = []
return childElement
.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
}
} else {
dLog(
"matchArrayAttribute [D\(depth)]: Unknown array key '\(key)'. This function needs to be extended for this key."
)
return false
}
if let actual = actualArray {
if Set(actual) != Set(expectedArray) {
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' expected '\(expectedArray)', but found '\(actual)'. Sets differ. No match."
)
return false
}
return true
} else {
if expectedArray.isEmpty {
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' not found, but expected array was empty. Match for this key."
)
return true
}
dLog(
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
)
return false
}
}
@MainActor
internal func matchBooleanAttribute(
element: Element,
key: String,
expectedValueString: String,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
var currentBoolValue: Bool?
switch key {
case AXAttributeNames.kAXEnabledAttribute: currentBoolValue = element.isEnabled(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case AXAttributeNames.kAXFocusedAttribute: currentBoolValue = element.isFocused(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case AXAttributeNames.kAXHiddenAttribute: currentBoolValue = element.isHidden(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case AXAttributeNames.kAXElementBusyAttribute: currentBoolValue = element.isElementBusy(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case AXMiscConstants.isIgnoredAttributeKey: currentBoolValue = element.isIgnored(
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
case AXAttributeNames.kAXMainAttribute: currentBoolValue = element.attribute(
Attribute<Bool>(key),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
default:
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
return false
}
if let actualBool = currentBoolValue {
let expectedBool = expectedValueString.lowercased() == "true"
if actualBool != expectedBool {
dLog(
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' expected '\(expectedBool)', but found '\(actualBool)'. No match."
)
return false
}
return true
} else {
dLog(
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
)
return false
}
}
@MainActor
internal func matchComputedNameAttributes(
element: Element,
computedNameEquals: String?,
computedNameContains: String?,
depth: Int,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if computedNameEquals == nil && computedNameContains == nil {
return true // No computed name criteria to match, so this part passes.
}
// getComputedAttributes will need logging parameters
let computedAttrs = getComputedAttributes(
for: element,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
)
// Assuming AXMiscConstants.computedNameAttributeKey is a globally defined constant string for the key like "computedName"
// And AttributeData has a `value: AnyCodable?` property
if let currentComputedNameAnyCodable = computedAttrs[AXMiscConstants.computedNameAttributeKey]?.value as? AnyCodable,
let currentComputedName = currentComputedNameAnyCodable.value as? String {
if let equals = computedNameEquals {
if currentComputedName != equals {
dLog(
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' != '\(equals)'. No match."
)
return false
}
}
if let contains = computedNameContains {
if !currentComputedName.localizedCaseInsensitiveContains(contains) {
dLog(
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' does not contain '\(contains)'. No match."
)
return false
}
}
return true
} else {
// Only log failure if there was a criteria for computed name.
if computedNameEquals != nil || computedNameContains != nil {
dLog(
"matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none or it's not a string. No match."
)
return false
}
return true // No criteria, and no computed name, effectively a pass for this sub-check.
}
}

View File

@ -0,0 +1,28 @@
// ErrorUtils.swift - Error handling utilities
import ApplicationServices // Added for AXError type
import Foundation
// Helper function to convert AXError to a string
public func axErrorToString(_ error: AXError) -> String {
switch error {
case .success: return "success"
case .failure: return "failure"
case .apiDisabled: return "apiDisabled"
case .invalidUIElement: return "invalidUIElement"
case .invalidUIElementObserver: return "invalidUIElementObserver"
case .cannotComplete: return "cannotComplete"
case .attributeUnsupported: return "attributeUnsupported"
case .actionUnsupported: return "actionUnsupported"
case .notificationUnsupported: return "notificationUnsupported"
case .notImplemented: return "notImplemented"
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
case .notificationNotRegistered: return "notificationNotRegistered"
case .noValue: return "noValue"
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
case .notEnoughPrecision: return "notEnoughPrecision"
case .illegalArgument: return "illegalArgument"
@unknown default:
return "unknown AXError (code: \(error.rawValue))"
}
}

View File

@ -2,9 +2,6 @@
import Foundation
// String extension MOVED to String+HelperExtensions.swift
// CustomCharacterSet struct MOVED to CustomCharacterSet.swift
// Scanner class from Scanner
class Scanner {

View File

@ -29,3 +29,23 @@ extension Optional {
}
}
}
extension String {
func truncated(to length: Int, trailing: String = "...") -> String {
if self.count > length {
return String(self.prefix(length - trailing.count)) + trailing
} else {
return self
}
}
private static let MAX_LOG_ABBREV_LENGTH = 50
func truncatedToMaxLogAbbrev() -> String {
if self.count > Self.MAX_LOG_ABBREV_LENGTH {
return String(self.prefix(Self.MAX_LOG_ABBREV_LENGTH - 3)) + "..."
} else {
return self
}
}
}

View File

@ -22,10 +22,10 @@ public func extractTextContent(
dLog("Extracting text content for element: \(elementDescription)")
var texts: [String] = []
let textualAttributes = [
kAXValueAttribute, kAXTitleAttribute, kAXDescriptionAttribute, kAXHelpAttribute,
kAXPlaceholderValueAttribute, kAXLabelValueAttribute, kAXRoleDescriptionAttribute
// Consider adding kAXStringForRangeParameterizedAttribute if dealing with large text views for performance
// kAXSelectedTextAttribute could also be relevant depending on use case
AXAttributeNames.kAXValueAttribute, AXAttributeNames.kAXTitleAttribute, AXAttributeNames.kAXDescriptionAttribute, AXAttributeNames.kAXHelpAttribute,
AXAttributeNames.kAXPlaceholderValueAttribute, AXAttributeNames.kAXLabelValueAttribute, AXAttributeNames.kAXRoleDescriptionAttribute
// Consider adding stringForRangeParameterizedAttribute if dealing with large text views for performance
// selectedTextAttribute could also be relevant depending on use case
]
for attrName in textualAttributes {
var tempLogs: [String] = [] // For the axValue call
@ -35,7 +35,7 @@ public func extractTextContent(
attr: attrName,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &tempLogs
), !strValue.isEmpty, strValue.lowercased() != kAXNotAvailableString.lowercased() {
), !strValue.isEmpty, strValue.lowercased() != AXMiscConstants.kAXNotAvailableString.lowercased() {
texts.append(strValue)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from axValue
} else {

View File

@ -0,0 +1,109 @@
// AXValueSpecificFormatter.swift - Formats AXValue types into strings
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize etc.
import Foundation
// Assumes ValueFormatOption is available (likely from ValueFormatter.swift or a shared Models.swift)
// Assumes stringFromAXValueType is available from ValueHelpers.swift and axErrorToString is available from ErrorUtils.swift
@MainActor
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .default) -> String {
let type = AXValueGetType(axValue)
// Handle special boolean type first (raw value 4)
// In some SDK versions or contexts, AXValueType might not have a direct .boolean case,
// or a specific attribute might return a boolean-like value encoded with rawValue 4.
if type.rawValue == 4 { // Check for boolean-like AXValue based on rawValue
return formatBooleanAXValue(axValue, type: type, option: option)
}
// Handle standard AXValue types
return formatStandardAXValue(axValue, type: type, option: option)
}
@MainActor
private func formatBooleanAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
var boolResult: DarwinBoolean = false // Use DarwinBoolean for AXValueGetValue
if AXValueGetValue(axValue, type, &boolResult) {
let result = boolResult.boolValue ? "true" : "false"
return option == .verbose ? "<Boolean: \(result)>" : result
}
// Fallback if AXValueGetValue fails
return "AXValue (\(stringFromAXValueType(type)))"
}
@MainActor
private func formatStandardAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
switch type {
case .cgPoint:
return formatCGPointAXValue(axValue, option: option)
case .cgSize:
return formatCGSizeAXValue(axValue, option: option)
case .cgRect:
return formatCGRectAXValue(axValue, option: option)
case .cfRange:
return formatCFRangeAXValue(axValue, option: option)
case .axError:
return formatAXErrorAXValue(axValue, option: option)
case .illegal:
return "Illegal AXValue"
@unknown default:
// Use stringFromAXValueType for unknown cases
return "AXValue (\(stringFromAXValueType(type)))"
}
}
@MainActor
private func formatCGPointAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var point = CGPoint.zero
if AXValueGetValue(axValue, .cgPoint, &point) {
let result = "x=\(point.x) y=\(point.y)"
return option == .verbose ? "<CGPoint: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgPoint)))"
}
@MainActor
private func formatCGSizeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var size = CGSize.zero
if AXValueGetValue(axValue, .cgSize, &size) {
let result = "w=\(size.width) h=\(size.height)"
return option == .verbose ? "<CGSize: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgSize)))"
}
@MainActor
private func formatCGRectAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var rect = CGRect.zero
if AXValueGetValue(axValue, .cgRect, &rect) {
let result = "x=\(rect.origin.x) y=\(rect.origin.y) w=\(rect.size.width) h=\(rect.size.height)"
return option == .verbose ? "<CGRect: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgRect)))"
}
@MainActor
private func formatCFRangeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var range = CFRange() // No .zero for CFRange, default init is fine.
if AXValueGetValue(axValue, .cfRange, &range) {
let result = "pos=\(range.location) len=\(range.length)"
return option == .verbose ? "<CFRange: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cfRange)))"
}
@MainActor
private func formatAXErrorAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var error = AXError.success
if AXValueGetValue(axValue, .axError, &error) {
let result = axErrorToString(error) // Assumes axErrorToString is available
return option == .verbose ? "<AXError: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.axError)))"
}
// stringFromAXValueType is available from ValueHelpers.swift
// axErrorToString is available from ErrorUtils.swift

View File

@ -0,0 +1,207 @@
// ValueCasters.swift - Contains type casting helper functions for AX values
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize
import Foundation
// Note: Assumes Element (for castToElementArray) is available.
@MainActor
internal func castValueToType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
// Handle basic types
if let result = castToBasicType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle array types
if let result = castToArrayType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle geometry types
if let result = castToGeometryType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle special types
if let result = castToSpecialType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Direct cast fallback
if let directCast = value as? T {
return directCast
}
dLog(
"axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. " +
"Unwrapped value was \(type(of: value)): \(value)"
)
return nil
}
@MainActor
internal func castToBasicType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is String.Type:
return castToString(value, attr: attr, dLog: dLog) as? T
case is Bool.Type:
return castToBool(value, attr: attr, dLog: dLog) as? T
case is Int.Type:
return castToInt(value, attr: attr, dLog: dLog) as? T
case is Double.Type:
return castToDouble(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
internal func castToString(_ value: Any, attr: String, dLog: (String) -> Void) -> String? {
if let str = value as? String {
return str
} else if let attrStr = value as? NSAttributedString {
return attrStr.string
}
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToBool(_ value: Any, attr: String, dLog: (String) -> Void) -> Bool? {
if let boolVal = value as? Bool {
return boolVal
} else if let numVal = value as? NSNumber {
return numVal.boolValue
}
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToInt(_ value: Any, attr: String, dLog: (String) -> Void) -> Int? {
if let intVal = value as? Int {
return intVal
} else if let numVal = value as? NSNumber {
return numVal.intValue
}
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToDouble(_ value: Any, attr: String, dLog: (String) -> Void) -> Double? {
if let doubleVal = value as? Double {
return doubleVal
} else if let numVal = value as? NSNumber {
return numVal.doubleValue
}
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToArrayType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is [AXUIElement].Type:
return castToAXUIElementArray(value, attr: attr, dLog: dLog) as? T
case is [Element].Type:
return castToElementArray(value, attr: attr, dLog: dLog) as? T
case is [String].Type:
return castToStringArray(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
internal func castToAXUIElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [AXUIElement]? {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> AXUIElement? in
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
return (cfItem as! AXUIElement)
}
return nil
}
return result
}
dLog("axValue: Expected [AXUIElement] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [Element]? {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> Element? in
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
return Element(cfItem as! AXUIElement) // Assumes Element initializer is public/internal
}
return nil
}
return result
}
dLog("axValue: Expected [Element] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToStringArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [String]? {
if let stringArray = value as? [Any?] {
let result = stringArray.compactMap { $0 as? String }
if result.count == stringArray.count {
return result
}
}
dLog("axValue: Expected [String] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToGeometryType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is CGPoint.Type:
return castToCGPoint(value, attr: attr, dLog: dLog) as? T
case is CGSize.Type:
return castToCGSize(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
internal func castToCGPoint(_ value: Any, attr: String, dLog: (String) -> Void) -> CGPoint? {
if let pointVal = value as? CGPoint {
return pointVal
}
dLog("axValue: Expected CGPoint for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToCGSize(_ value: Any, attr: String, dLog: (String) -> Void) -> CGSize? {
if let sizeVal = value as? CGSize {
return sizeVal
}
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
internal func castToSpecialType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
if expectedType == AXUIElement.self {
return castToAXUIElement(value, attr: attr, dLog: dLog) as? T
}
return nil
}
@MainActor
internal func castToAXUIElement(_ value: Any, attr: String, dLog: (String) -> Void) -> AXUIElement? {
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
return (cfValue as! AXUIElement)
}
let typeDescription = String(describing: type(of: value))
let valueDescription = String(describing: value)
dLog("axValue: Expected AXUIElement for attribute '\(attr)', but got \(typeDescription): \(valueDescription)")
return nil
}

View File

@ -11,101 +11,6 @@ public enum ValueFormatOption {
// Add more variants as needed, like .minimal, .debug, etc.
}
// MARK: - AXValue Formatting
@MainActor
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .default) -> String {
let type = AXValueGetType(axValue)
// Handle special boolean type first
if type.rawValue == 4 {
return formatBooleanAXValue(axValue, type: type, option: option)
}
// Handle standard AXValue types
return formatStandardAXValue(axValue, type: type, option: option)
}
@MainActor
private func formatBooleanAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
var boolResult: DarwinBoolean = false
if AXValueGetValue(axValue, type, &boolResult) {
let result = boolResult.boolValue ? "true" : "false"
return option == .verbose ? "<Boolean: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(type)))"
}
@MainActor
private func formatStandardAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
switch type {
case .cgPoint:
return formatCGPointAXValue(axValue, option: option)
case .cgSize:
return formatCGSizeAXValue(axValue, option: option)
case .cgRect:
return formatCGRectAXValue(axValue, option: option)
case .cfRange:
return formatCFRangeAXValue(axValue, option: option)
case .axError:
return formatAXErrorAXValue(axValue, option: option)
case .illegal:
return "Illegal AXValue"
default:
return "AXValue (\(stringFromAXValueType(type)))"
}
}
@MainActor
private func formatCGPointAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var point = CGPoint.zero
if AXValueGetValue(axValue, .cgPoint, &point) {
let result = "x=\(point.x) y=\(point.y)"
return option == .verbose ? "<CGPoint: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgPoint)))"
}
@MainActor
private func formatCGSizeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var size = CGSize.zero
if AXValueGetValue(axValue, .cgSize, &size) {
let result = "w=\(size.width) h=\(size.height)"
return option == .verbose ? "<CGSize: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgSize)))"
}
@MainActor
private func formatCGRectAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var rect = CGRect.zero
if AXValueGetValue(axValue, .cgRect, &rect) {
let result = "x=\(rect.origin.x) y=\(rect.origin.y) w=\(rect.size.width) h=\(rect.size.height)"
return option == .verbose ? "<CGRect: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cgRect)))"
}
@MainActor
private func formatCFRangeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var range = CFRange()
if AXValueGetValue(axValue, .cfRange, &range) {
let result = "pos=\(range.location) len=\(range.length)"
return option == .verbose ? "<CFRange: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.cfRange)))"
}
@MainActor
private func formatAXErrorAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var error = AXError.success
if AXValueGetValue(axValue, .axError, &error) {
let result = axErrorToString(error)
return option == .verbose ? "<AXError: \(result)>" : result
}
return "AXValue (\(stringFromAXValueType(.axError)))"
}
// MARK: - CFTypeRef Formatting
@MainActor
@ -171,11 +76,11 @@ private func formatAXUIElement(
currentDebugLogs: inout [String]
) -> String {
let element = Element(value as! AXUIElement)
// Create a simple description using available element properties
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "Unknown"
let title = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let title = title, !title.isEmpty {
return option == .verbose ? "<\(role): \"\(title)\">" : "\(role):\"\(title)\""
} else {
@ -192,7 +97,7 @@ private func formatCFArray(
) -> String {
let cfArray = value as! CFArray
let count = CFArrayGetCount(cfArray)
if option == .verbose || count <= 5 {
var swiftArray: [String] = []
for index in 0..<count {
@ -222,7 +127,7 @@ private func formatCFDictionary(
) -> String {
let cfDict = value as! CFDictionary
let count = CFDictionaryGetCount(cfDict)
if option == .verbose || count <= 3 {
var swiftDict: [String: String] = [:]
if let nsDict = cfDict as? [String: AnyObject] {

View File

@ -3,7 +3,7 @@ import CoreGraphics // For CGPoint, CGSize etc.
import Foundation
// debug() is assumed to be globally available from Logging.swift
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
// ValueUnwrapper has been moved to its own file: ValueUnwrapper.swift
@ -44,211 +44,10 @@ public func axValue<T>(
return nil
}
// Call castValueToType from ValueCasters.swift
return castValueToType(value, expectedType: T.self, attr: attr, dLog: dLog)
}
// MARK: - Type Casting Helpers
@MainActor
private func castValueToType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
// Handle basic types
if let result = castToBasicType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle array types
if let result = castToArrayType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle geometry types
if let result = castToGeometryType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Handle special types
if let result = castToSpecialType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
return result
}
// Direct cast fallback
if let directCast = value as? T {
return directCast
}
dLog(
"axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. " +
"Unwrapped value was \(type(of: value)): \(value)"
)
return nil
}
@MainActor
private func castToBasicType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is String.Type:
return castToString(value, attr: attr, dLog: dLog) as? T
case is Bool.Type:
return castToBool(value, attr: attr, dLog: dLog) as? T
case is Int.Type:
return castToInt(value, attr: attr, dLog: dLog) as? T
case is Double.Type:
return castToDouble(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
private func castToString(_ value: Any, attr: String, dLog: (String) -> Void) -> String? {
if let str = value as? String {
return str
} else if let attrStr = value as? NSAttributedString {
return attrStr.string
}
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToBool(_ value: Any, attr: String, dLog: (String) -> Void) -> Bool? {
if let boolVal = value as? Bool {
return boolVal
} else if let numVal = value as? NSNumber {
return numVal.boolValue
}
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToInt(_ value: Any, attr: String, dLog: (String) -> Void) -> Int? {
if let intVal = value as? Int {
return intVal
} else if let numVal = value as? NSNumber {
return numVal.intValue
}
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToDouble(_ value: Any, attr: String, dLog: (String) -> Void) -> Double? {
if let doubleVal = value as? Double {
return doubleVal
} else if let numVal = value as? NSNumber {
return numVal.doubleValue
}
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToArrayType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is [AXUIElement].Type:
return castToAXUIElementArray(value, attr: attr, dLog: dLog) as? T
case is [Element].Type:
return castToElementArray(value, attr: attr, dLog: dLog) as? T
case is [String].Type:
return castToStringArray(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
private func castToAXUIElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [AXUIElement]? {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> AXUIElement? in
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
return (cfItem as! AXUIElement)
}
return nil
}
return result
}
dLog("axValue: Expected [AXUIElement] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [Element]? {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> Element? in
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
return Element(cfItem as! AXUIElement)
}
return nil
}
return result
}
dLog("axValue: Expected [Element] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToStringArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [String]? {
if let stringArray = value as? [Any?] {
let result = stringArray.compactMap { $0 as? String }
if result.count == stringArray.count {
return result
}
}
dLog("axValue: Expected [String] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToGeometryType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
switch expectedType {
case is CGPoint.Type:
return castToCGPoint(value, attr: attr, dLog: dLog) as? T
case is CGSize.Type:
return castToCGSize(value, attr: attr, dLog: dLog) as? T
default:
return nil
}
}
@MainActor
private func castToCGPoint(_ value: Any, attr: String, dLog: (String) -> Void) -> CGPoint? {
if let pointVal = value as? CGPoint {
return pointVal
}
dLog("axValue: Expected CGPoint for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToCGSize(_ value: Any, attr: String, dLog: (String) -> Void) -> CGSize? {
if let sizeVal = value as? CGSize {
return sizeVal
}
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
@MainActor
private func castToSpecialType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
if expectedType == AXUIElement.self {
return castToAXUIElement(value, attr: attr, dLog: dLog) as? T
}
return nil
}
@MainActor
private func castToAXUIElement(_ value: Any, attr: String, dLog: (String) -> Void) -> AXUIElement? {
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
return (cfValue as! AXUIElement)
}
let typeDescription = String(describing: type(of: value))
let valueDescription = String(describing: value)
dLog("axValue: Expected AXUIElement for attribute '\(attr)', but got \(typeDescription): \(valueDescription)")
return nil
}
// MARK: - AXValueType String Helper
public func stringFromAXValueType(_ type: AXValueType) -> String {

View File

@ -5,7 +5,7 @@ import CoreGraphics // For CGPoint, CGSize, CGRect, CFRange
import Foundation
// debug() is assumed to be globally available from Logging.swift
// Constants are assumed to be globally available from AccessibilityConstants.swift
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
// Scanner and CustomCharacterSet are from Scanner.swift
// AccessibilityError is from AccessibilityError.swift
@ -90,7 +90,7 @@ public func createCFTypeRefFromString(stringValue: String, forElement element: E
@MainActor
private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValueType, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> AXValue? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let valueRef: AXValue?
switch targetAXValueType {
case .cgPoint:
@ -124,7 +124,7 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
private func parseCGPoint(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
var x: Double = 0, y: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let xValStr = components[0].split(separator: "=").last, let xVal = Double(xValStr),
let yValStr = components[1].split(separator: "=").last, let yVal = Double(yValStr) {
@ -152,7 +152,7 @@ private func parseCGPoint(from stringValue: String, dLog: (String) -> Void) thro
private func parseCGSize(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
var w: Double = 0, h: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let wValStr = components[0].split(separator: "=").last, let wVal = Double(wValStr),
let hValStr = components[1].split(separator: "=").last, let hVal = Double(hValStr) {
@ -180,7 +180,7 @@ private func parseCGSize(from stringValue: String, dLog: (String) -> Void) throw
private func parseCGRect(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
var x: Double = 0, y: Double = 0, w: Double = 0, h: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 4,
let xStr = components[0].split(separator: "=").last, let xVal = Double(xStr),
let yStr = components[1].split(separator: "=").last, let yVal = Double(yStr),
@ -216,7 +216,7 @@ private func parseCGRect(from stringValue: String, dLog: (String) -> Void) throw
private func parseCFRange(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
var loc: Int = 0, len: Int = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let locStr = components[0].split(separator: "=").last, let locVal = Int(locStr),
let lenStr = components[1].split(separator: "=").last, let lenVal = Int(lenStr) {
@ -245,10 +245,10 @@ private func parseCFRange(from stringValue: String, dLog: (String) -> Void) thro
private func parseDefaultAXValueType(from stringValue: String, targetType: AXValueType, dLog: (String) -> Void) throws -> AXValue? {
if targetType.rawValue == 4 {
var boolVal: DarwinBoolean
if stringValue.lowercased() == "true" {
boolVal = true
} else if stringValue.lowercased() == "false" {
boolVal = false
if stringValue.lowercased() == "true" {
boolVal = true
} else if stringValue.lowercased() == "false" {
boolVal = false
} else {
dLog("parseStringToAXValue: Boolean parsing failed for '\(stringValue)' for AXValue.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as boolean for AXValue.")

View File

@ -3,7 +3,7 @@ import CoreGraphics // For CGPoint, CGSize etc.
import Foundation
// debug() is assumed to be globally available from Logging.swift
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
// MARK: - ValueUnwrapper Utility
struct ValueUnwrapper {

View File

@ -20,8 +20,11 @@ struct AXORCCommand: AsyncParsableCommand {
@Option(name: .long, help: "Read JSON payload from the specified file path.")
var file: String?
@Option(name: .long, help: "Read JSON payload directly from this string argument, expecting a JSON string.")
var json: String?
@Argument(
help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file) are used, this argument is ignored."
help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file, --json) are used, this argument is ignored."
)
var directPayload: String?
@ -30,6 +33,7 @@ struct AXORCCommand: AsyncParsableCommand {
let inputResult = InputHandler.parseInput(
stdin: stdin,
file: file,
json: json,
directPayload: directPayload,
debug: debug
)
@ -102,19 +106,33 @@ struct AXORCCommand: AsyncParsableCommand {
print(result)
} catch {
// FORCED DEBUGGING FOR THIS ERROR PATH
// debug = true // Temporarily enable debug logs for this error block if needed
var errorSpecificDebugLogs = localDebugLogs // Copy existing logs
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Original jsonString that led to this error: [\(jsonString)]")
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: jsonData.count that led to this error: \(jsonData.count)")
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Raw error.localizedDescription: \(error.localizedDescription)")
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Full error object: \(error)")
let errorMessage = "Failed to parse JSON command. Raw Error: \(error.localizedDescription). JSON Input (first 100 chars): \(jsonString.prefix(100))..."
let errorResponse = ErrorResponse(
command_id: "decode_error",
error: ErrorResponse.ErrorDetail(
message: "Failed to parse JSON command: \(error.localizedDescription)"
message: errorMessage
),
debug_logs: debug ? localDebugLogs : nil
// Always include these enhanced debug logs for decode_error for now
debug_logs: errorSpecificDebugLogs
)
if let jsonData = try? JSONEncoder().encode(errorResponse),
let jsonStr = String(data: jsonData, encoding: .utf8) {
print(jsonStr)
if let responseData = try? JSONEncoder().encode(errorResponse),
let responseStr = String(data: responseData, encoding: .utf8) {
print(responseStr)
} else {
print("{\"error\": \"Failed to encode error response\"}")
// Fallback if even error encoding fails
let fallbackErrorMsg = "{\"error\": \"Failed to encode error response. Original error for decode: \(error.localizedDescription). Input was: \(jsonString)\"}"
print(fallbackErrorMsg)
}
}
}

View File

@ -27,171 +27,106 @@ struct CommandExecutor {
switch command.command {
case .performAction:
guard let actionName = command.action_name, let locator = command.locator else {
let error = "Missing action_name or locator for performAction"
guard let actionName = command.action_name else {
let error = "Missing action_name for performAction"
localDebugLogs.append(error)
return encodeToJson(QueryResponse(
success: false,
commandId: command.command_id,
command: command.command.rawValue,
error: error,
debugLogs: debug ? localDebugLogs : nil
debugLogs: effectiveDebugLogging ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding error response failed\"}"
}
let handlerResponse: HandlerResponse = await ax.handlePerformAction(
for: command.application,
locator: locator,
pathHint: command.path_hint,
actionName: actionName,
actionValue: command.action_value,
maxDepth: command.max_elements,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executePerformAction(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs,
actionName: actionName
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding performAction response failed\"}"
case .getFocusedElement:
let handlerResponse: HandlerResponse = await ax.handleGetFocusedElement(
for: command.application,
requestedAttributes: command.attributes,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executeGetFocusedElement(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding getFocusedElement response failed\"}"
case .getAttributes:
guard let locator = command.locator else {
let error = "Missing locator for getAttributes"
localDebugLogs.append(error)
return encodeToJson(QueryResponse(
success: false,
commandId: command.command_id,
command: command.command.rawValue,
error: error,
debugLogs: debug ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding error response failed\"}"
}
let handlerResponse: HandlerResponse = await ax.handleGetAttributes(
for: command.application,
locator: locator,
requestedAttributes: command.attributes,
pathHint: command.path_hint,
maxDepth: command.max_elements,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executeGetAttributes(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding getAttributes response failed\"}"
case .query:
guard let locator = command.locator else {
let error = "Missing locator for query"
localDebugLogs.append(error)
return encodeToJson(QueryResponse(
success: false,
commandId: command.command_id,
command: command.command.rawValue,
error: error,
debugLogs: debug ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding error response failed\"}"
}
let handlerResponse: HandlerResponse = await ax.handleQuery(
for: command.application,
locator: locator,
pathHint: command.path_hint,
maxDepth: command.max_elements,
requestedAttributes: command.attributes,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executeQuery(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding query response failed\"}"
case .describeElement:
guard let locator = command.locator else {
let error = "Missing locator for describeElement"
localDebugLogs.append(error)
return encodeToJson(QueryResponse(
success: false,
commandId: command.command_id,
command: command.command.rawValue,
error: error,
debugLogs: debug ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding error response failed\"}"
}
let handlerResponse: HandlerResponse = await ax.handleDescribeElement(
for: command.application,
locator: locator,
pathHint: command.path_hint,
maxDepth: command.max_elements,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executeDescribeElement(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding describeElement response failed\"}"
case .extractText:
guard let locator = command.locator else {
let error = "Missing locator for extractText"
localDebugLogs.append(error)
return encodeToJson(QueryResponse(
success: false,
commandId: command.command_id,
command: command.command.rawValue,
error: error,
debugLogs: debug ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding error response failed\"}"
}
let handlerResponse: HandlerResponse = await ax.handleExtractText(
for: command.application,
locator: locator,
pathHint: command.path_hint,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
let handlerResponse = await Self.executeExtractText(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
let queryResponse = QueryResponse(
command_id: command.command_id,
success: handlerResponse.error == nil,
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding extractText response failed\"}"
case .collectAll:
let jsonStringResult = await ax.handleCollectAll(
@ -208,56 +143,239 @@ struct CommandExecutor {
return jsonStringResult
case .batch:
guard let subCommands = command.sub_commands else {
let error = "Missing sub_commands for batch command"
localDebugLogs.append(error)
return encodeToJson(BatchResponse(
command_id: command.command_id,
success: false,
results: [],
error: error,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
)) ?? "{\"error\": \"Encoding batch error response failed\"}"
}
var batchDebugLogs = localDebugLogs
let batchResults: [HandlerResponse] = await ax.handleBatchCommands(
batchCommandID: command.command_id,
subCommands: subCommands,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &batchDebugLogs
)
let overallSuccess = batchResults.allSatisfy { $0.error == nil }
let batchResponse = BatchResponse(
command_id: command.command_id,
success: overallSuccess,
results: batchResults,
error: nil,
debug_logs: effectiveDebugLogging ? batchDebugLogs : nil
let batchResponse = await Self.executeBatch(
command: command,
ax: ax,
effectiveDebugLogging: effectiveDebugLogging,
localDebugLogs: &localDebugLogs
)
return encodeToJson(batchResponse) ?? "{\"error\": \"Encoding batch response failed\"}"
case .ping:
if effectiveDebugLogging { localDebugLogs.append("Ping command received. Responding with pong.") }
// Create an empty HandlerResponse for ping
let pingHandlerResponse = HandlerResponse(
data: nil,
error: nil,
debug_logs: nil // Ping-specific logs are already in localDebugLogs
data: nil,
error: nil,
debug_logs: nil
)
// Construct QueryResponse using the HandlerResponse initializer
let queryResponse = QueryResponse(
command_id: command.command_id,
success: true, // Ping is always a success if reached
command: command.command.rawValue,
return Self.finalizeAndEncodeResponse(
commandId: command.command_id,
commandType: command.command.rawValue,
handlerResponse: pingHandlerResponse,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
localDebugLogs: localDebugLogs,
effectiveDebugLogging: effectiveDebugLogging
)
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding ping response failed\"}"
}
}
// MARK: - Command Execution Functions
private static func executePerformAction(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String],
actionName: String
) async -> HandlerResponse {
// Locator is now optional for this path, AXorcist.handlePerformAction will use path_hint if locator is nil
return await ax.handlePerformAction(
for: command.application,
locator: command.locator, // This can be nil
pathHint: command.path_hint,
actionName: actionName,
actionValue: command.action_value,
maxDepth: command.max_elements,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeGetFocusedElement(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> HandlerResponse {
return await ax.handleGetFocusedElement(
for: command.application,
requestedAttributes: command.attributes,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeGetAttributes(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for getAttributes"
localDebugLogs.append(error)
return HandlerResponse(
data: nil,
error: error,
debug_logs: nil
)
}
return await ax.handleGetAttributes(
for: command.application,
locator: locator,
requestedAttributes: command.attributes,
pathHint: command.path_hint,
maxDepth: command.max_elements,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeQuery(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for query"
localDebugLogs.append(error)
return HandlerResponse(
data: nil,
error: error,
debug_logs: nil
)
}
return await ax.handleQuery(
for: command.application,
locator: locator,
pathHint: command.path_hint,
maxDepth: command.max_elements,
requestedAttributes: command.attributes,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeDescribeElement(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for describeElement"
localDebugLogs.append(error)
return HandlerResponse(
data: nil,
error: error,
debug_logs: nil
)
}
return await ax.handleDescribeElement(
for: command.application,
locator: locator,
pathHint: command.path_hint,
maxDepth: command.max_elements,
requestedAttributes: command.attributes,
outputFormat: command.output_format,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeExtractText(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> HandlerResponse {
guard let locator = command.locator else {
let error = "Missing locator for extractText"
localDebugLogs.append(error)
return HandlerResponse(
data: nil,
error: error,
debug_logs: nil
)
}
return await ax.handleExtractText(
for: command.application,
locator: locator,
pathHint: command.path_hint,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &localDebugLogs
)
}
private static func executeBatch(
command: CommandEnvelope,
ax: AXorcist,
effectiveDebugLogging: Bool,
localDebugLogs: inout [String]
) async -> BatchResponse {
guard let subCommands = command.sub_commands else {
let error = "Missing sub_commands for batch command"
localDebugLogs.append(error)
return BatchResponse(
command_id: command.command_id,
success: false,
results: [],
error: error,
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
)
}
var batchDebugLogs = localDebugLogs
let batchResults: [HandlerResponse] = await ax.handleBatchCommands(
batchCommandID: command.command_id,
subCommands: subCommands,
isDebugLoggingEnabled: effectiveDebugLogging,
currentDebugLogs: &batchDebugLogs
)
let overallSuccess = batchResults.allSatisfy { $0.error == nil }
return BatchResponse(
command_id: command.command_id,
success: overallSuccess,
results: batchResults,
error: nil,
debug_logs: effectiveDebugLogging ? batchDebugLogs : nil
)
}
// MARK: - Helper Functions
private static func finalizeAndEncodeResponse(
commandId: String,
commandType: String,
handlerResponse: HandlerResponse,
localDebugLogs: [String],
effectiveDebugLogging: Bool
) -> String {
// Combine debug logs if debug logging is enabled
var combinedDebugLogs: [String]?
if effectiveDebugLogging {
combinedDebugLogs = localDebugLogs
if let handlerDebugLogs = handlerResponse.debug_logs {
combinedDebugLogs?.append(contentsOf: handlerDebugLogs)
}
}
// Create QueryResponse
let queryResponse = QueryResponse(
command_id: commandId,
success: handlerResponse.error == nil,
command: commandType,
handlerResponse: handlerResponse,
debug_logs: combinedDebugLogs
)
// Encode to JSON and return
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding \(commandType) response failed\"}"
}
private static func encodeToJson<T: Codable>(_ object: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
@ -272,4 +390,4 @@ struct CommandExecutor {
return nil
}
}
}
}

View File

@ -7,48 +7,60 @@ struct InputHandler {
static func parseInput(
stdin: Bool,
file: String?,
json: String?,
directPayload: String?,
debug: Bool
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
var localDebugLogs: [String] = []
if debug {
localDebugLogs.append("Debug logging enabled by --debug flag.")
}
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0)
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0) + (json != nil ? 1 : 0)
let positionalPayloadProvided = directPayload != nil &&
!(directPayload?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
if activeInputFlags > 1 {
return handleMultipleInputFlags(&localDebugLogs)
} else if stdin {
return handleStdinInput(debug: debug, debugLogs: &localDebugLogs)
} else if let filePath = file {
return handleFileInput(filePath: filePath, debug: debug, debugLogs: &localDebugLogs)
} else if let jsonString = json {
if debug {
localDebugLogs.append("Using --json flag with payload of \\(jsonString.count) characters.")
}
let trimmedJsonString = jsonString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedJsonString.isEmpty {
let error = "Error: --json argument was provided but the string is empty or only whitespace."
if debug { localDebugLogs.append(error) }
return (nil, "--json argument", error, localDebugLogs)
}
return (trimmedJsonString, "--json argument", nil, localDebugLogs)
} else if positionalPayloadProvided {
return handleDirectPayload(directPayload: directPayload, debug: debug, debugLogs: &localDebugLogs)
} else {
return handleNoInput(debug: debug, debugLogs: &localDebugLogs)
}
}
// MARK: - Helper Functions
private static func handleMultipleInputFlags(
_ debugLogs: inout [String]
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
let error = "Error: Multiple input flags specified (--stdin, --file). Only one is allowed."
let error = "Error: Multiple input flags specified (--stdin, --file, --json). Only one is allowed."
return (nil, error, error, debugLogs)
}
private static func handleStdinInput(
debug: Bool,
debugLogs: inout [String]
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
let stdInputHandle = FileHandle.standardInput
let stdinData = stdInputHandle.readDataToEndOfFile()
if let str = String(data: stdinData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
!str.isEmpty {
if debug {
@ -63,14 +75,14 @@ struct InputHandler {
return (nil, "STDIN", error, debugLogs)
}
}
private static func handleFileInput(
filePath: String,
debug: Bool,
debugLogs: inout [String]
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
let sourceDescription = "File: \(filePath)"
do {
let rawFileContent = try String(contentsOfFile: filePath, encoding: .utf8) // Read raw
if debug {
@ -81,7 +93,7 @@ struct InputHandler {
if debug {
debugLogs.append("HFI_DEBUG: Trimmed file content: '\(str)' (length: \(str.count))")
}
if !str.isEmpty {
if debug {
debugLogs.append("Successfully read \(str.count) characters from file: \(filePath)")
@ -102,7 +114,7 @@ struct InputHandler {
return (nil, sourceDescription, errorMsg, debugLogs)
}
}
private static func handleDirectPayload(
directPayload: String?,
debug: Bool,
@ -114,12 +126,12 @@ struct InputHandler {
}
return (jsonString, "Direct argument", nil, debugLogs)
}
private static func handleNoInput(
debug: Bool,
debugLogs: inout [String]
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
let error = "No input provided. Use --stdin, --file <path>, or provide JSON as a direct argument."
let error = "No input provided. Use --stdin, --file <path>, --json <json_string>, or provide JSON as a direct argument."
if debug {
debugLogs.append("No input method specified and no direct payload provided.")
}

View File

@ -1,5 +1,5 @@
import AppKit
@testable import AXorcist
@testable import AXorcistLib
import Testing
import XCTest

78
axorc_runner.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# Simple runner for axorc, taking a JSON file as input.
# AXORC_PATH should be the path to your axorc executable.
# If not set, it defaults to a path relative to this script.
# Determine the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Set AXORC_PATH relative to the script's directory if not already set
: ${AXORC_PATH:="$SCRIPT_DIR/AXorcist/.build/debug/axorc"}
# Check if AXORC_PATH exists and is executable
if [ ! -x "$AXORC_PATH" ]; then
echo "Error: axorc executable not found or not executable at $AXORC_PATH"
echo "Please set AXORC_PATH environment variable or ensure it's built at the default location."
exit 1
fi
DEBUG_FLAG=""
POSITIONAL_ARGS=()
# Parse arguments for --debug and file/json payload
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_FLAG="--debug"
shift # past argument
;;
--file)
if [[ -z "$2" || ! -f "$2" ]]; then
echo "Error: File not provided or not found after --file argument."
exit 1
fi
INPUT_JSON=$(cat "$2")
USE_STDIN_FLAG=true
shift # past argument
shift # past value
;;
--json)
if [[ -z "$2" ]]; then
echo "Error: JSON string not provided after --json argument."
exit 1
fi
INPUT_JSON="$2"
USE_STDIN_FLAG=true
shift # past argument
shift # past value
;;
*)
POSITIONAL_ARGS+=("$1") # unknown option will be captured if axorc supports more
shift # past argument
;;
esac
done
if [ -z "$INPUT_JSON" ]; then
echo "Error: No JSON input provided via --file or --json."
echo "Usage: $0 [--debug] --file /path/to/command.json OR $0 [--debug] --json '{"command":"ping"}'"
exit 1
fi
echo "--- DEBUG_RUNNER: INPUT_JSON content before piping --- BEGIN"
printf "%s\n" "$INPUT_JSON"
echo "--- DEBUG_RUNNER: INPUT_JSON content before piping --- END"
echo "--- DEBUG_RUNNER: AXORC_PATH: $AXORC_PATH"
echo "--- DEBUG_RUNNER: DEBUG_FLAG: $DEBUG_FLAG"
# Execute axorc with the input JSON
if [ "$USE_STDIN_FLAG" = true ]; then
printf '%s' "$INPUT_JSON" | "$AXORC_PATH" --stdin $DEBUG_FLAG "${POSITIONAL_ARGS[@]}"
AXORC_EXIT_CODE=$?
echo "--- DEBUG_RUNNER: axorc exit code: $AXORC_EXIT_CODE ---"
else
# This case should not be reached if --file or --json is mandatory
echo "Error: USE_STDIN_FLAG was not set, programming error in runner script."
exit 1
fi