From ffd784a61d67f1e5879868d50ebb0c0a606ed065 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 May 2025 01:09:55 +0200 Subject: [PATCH] Lerge refactor --- .vscode/launch.json | 22 + Package.resolved | 54 --- .../AXorcist/AXorcist+CommandHandlers.swift | 2 +- Sources/AXorcist/AXorcist.swift | 185 ++----- .../AXorcist/Core/AXActionNameConstants.swift | 17 + .../Core/AXAttributeNameConstants.swift | 112 +++++ Sources/AXorcist/Core/AXMiscConstants.swift | 23 + .../AXorcist/Core/AXRoleNameConstants.swift | 43 ++ .../Core/AccessibilityConstants.swift | 203 -------- Sources/AXorcist/Core/AnyCodable.swift | 89 ++++ Sources/AXorcist/Core/Attribute.swift | 98 ++-- Sources/AXorcist/Core/CommandModels.swift | 75 +++ Sources/AXorcist/Core/DataModels.swift | 40 ++ Sources/AXorcist/Core/Element+Actions.swift | 73 +++ .../AXorcist/Core/Element+Description.swift | 85 ++++ Sources/AXorcist/Core/Element+Hierarchy.swift | 150 ++++-- .../Element+ParameterizedAttributes.swift | 80 +++ .../Core/Element+PathGeneration.swift | 169 +++++++ .../AXorcist/Core/Element+Properties.swift | 26 +- Sources/AXorcist/Core/Element.swift | 390 ++------------- Sources/AXorcist/Core/ElementFactories.swift | 37 ++ Sources/AXorcist/Core/ModelEnums.swift | 25 + Sources/AXorcist/Core/Models.swift | 376 --------------- Sources/AXorcist/Core/PathUtils.swift | 2 +- Sources/AXorcist/Core/ResponseModels.swift | 176 +++++++ .../Handlers/AXorcist+ActionHandlers.swift | 385 ++++++++------- .../Handlers/AXorcist+BatchHandler.swift | 15 +- .../Handlers/AXorcist+CollectAllHandler.swift | 25 +- .../Handlers/AXorcist+HandlerUtils.swift | 145 ++++++ .../Handlers/AXorcist+QueryHandlers.swift | 238 ++++----- Sources/AXorcist/Handlers/QueryHandlers.swift | 408 ---------------- .../AXorcist/Search/AttributeFormatter.swift | 67 +++ .../AXorcist/Search/AttributeHelpers.swift | 167 ++----- .../AXorcist/Search/AttributeMatcher.swift | 236 +-------- Sources/AXorcist/Search/ElementSearch.swift | 432 +++++++++-------- Sources/AXorcist/Search/PathNavigator.swift | 147 ++++++ .../AXorcist/Search/SearchCriteriaUtils.swift | 89 ++++ Sources/AXorcist/Search/SearchPathUtils.swift | 4 +- .../Search/SpecificAttributeMatchers.swift | 237 +++++++++ Sources/AXorcist/Utils/ErrorUtils.swift | 28 ++ Sources/AXorcist/Utils/Scanner.swift | 3 - .../Utils/String+HelperExtensions.swift | 20 + Sources/AXorcist/Utils/TextExtraction.swift | 10 +- .../Values/AXValueSpecificFormatter.swift | 109 +++++ Sources/AXorcist/Values/ValueCasters.swift | 207 ++++++++ Sources/AXorcist/Values/ValueFormatter.swift | 103 +--- Sources/AXorcist/Values/ValueHelpers.swift | 205 +------- Sources/AXorcist/Values/ValueParser.swift | 20 +- Sources/AXorcist/Values/ValueUnwrapper.swift | 2 +- Sources/axorc/AXORCMain.swift | 32 +- Sources/axorc/Core/CommandExecutor.swift | 454 +++++++++++------- Sources/axorc/Core/InputHandler.swift | 42 +- .../AXorcistIntegrationTests.swift | 2 +- axorc_runner.sh | 78 +++ 54 files changed, 3360 insertions(+), 3102 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 Sources/AXorcist/Core/AXActionNameConstants.swift create mode 100644 Sources/AXorcist/Core/AXAttributeNameConstants.swift create mode 100644 Sources/AXorcist/Core/AXMiscConstants.swift create mode 100644 Sources/AXorcist/Core/AXRoleNameConstants.swift delete mode 100644 Sources/AXorcist/Core/AccessibilityConstants.swift create mode 100644 Sources/AXorcist/Core/AnyCodable.swift create mode 100644 Sources/AXorcist/Core/CommandModels.swift create mode 100644 Sources/AXorcist/Core/DataModels.swift create mode 100644 Sources/AXorcist/Core/Element+Actions.swift create mode 100644 Sources/AXorcist/Core/Element+Description.swift create mode 100644 Sources/AXorcist/Core/Element+ParameterizedAttributes.swift create mode 100644 Sources/AXorcist/Core/Element+PathGeneration.swift create mode 100644 Sources/AXorcist/Core/ElementFactories.swift create mode 100644 Sources/AXorcist/Core/ModelEnums.swift delete mode 100644 Sources/AXorcist/Core/Models.swift create mode 100644 Sources/AXorcist/Core/ResponseModels.swift create mode 100644 Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift delete mode 100644 Sources/AXorcist/Handlers/QueryHandlers.swift create mode 100644 Sources/AXorcist/Search/AttributeFormatter.swift create mode 100644 Sources/AXorcist/Search/PathNavigator.swift create mode 100644 Sources/AXorcist/Search/SearchCriteriaUtils.swift create mode 100644 Sources/AXorcist/Search/SpecificAttributeMatchers.swift create mode 100644 Sources/AXorcist/Utils/ErrorUtils.swift create mode 100644 Sources/AXorcist/Values/AXValueSpecificFormatter.swift create mode 100644 Sources/AXorcist/Values/ValueCasters.swift create mode 100755 axorc_runner.sh diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8d4b741 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 284feb7..0fb601d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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 diff --git a/Sources/AXorcist/AXorcist+CommandHandlers.swift b/Sources/AXorcist/AXorcist+CommandHandlers.swift index 09f9ef2..c93c32e 100644 --- a/Sources/AXorcist/AXorcist+CommandHandlers.swift +++ b/Sources/AXorcist/AXorcist+CommandHandlers.swift @@ -39,7 +39,7 @@ extension AXorcist { var cfValue: CFTypeRef? let copyAttributeStatus = AXUIElementCopyAttributeValue( appElement.underlyingElement, - kAXFocusedUIElementAttribute as CFString, + AXAttributeNames.kAXFocusedUIElementAttribute as CFString, &cfValue ) diff --git a/Sources/AXorcist/AXorcist.swift b/Sources/AXorcist/AXorcist.swift index 3decd42..8f6db60 100644 --- a/Sources/AXorcist/AXorcist.swift +++ b/Sources/AXorcist/AXorcist.swift @@ -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(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { - // logLocal("currentElementMatchesPathComponent: Element \(element.briefDescription(option: .minimal, isDebugLoggingEnabled: false, currentDebugLogs: ¤tDebugLogs)) 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: ¤tDebugLogs // 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: ¤tDebugLogs) - 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: ¤tDebugLogs) { - dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)") - for child in childrenFromElementDotChildren { - let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) - if let actualValue = child.attribute(Attribute(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { - 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: ¤tDebugLogs) : "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: ¤tDebugLogs) - if let actualValue = childElement.attribute(Attribute(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { - 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: ¤tDebugLogs))", 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: ¤tDebugLogs))") - 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) } } diff --git a/Sources/AXorcist/Core/AXActionNameConstants.swift b/Sources/AXorcist/Core/AXActionNameConstants.swift new file mode 100644 index 0000000..0672981 --- /dev/null +++ b/Sources/AXorcist/Core/AXActionNameConstants.swift @@ -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" +} diff --git a/Sources/AXorcist/Core/AXAttributeNameConstants.swift b/Sources/AXorcist/Core/AXAttributeNameConstants.swift new file mode 100644 index 0000000..117b20f --- /dev/null +++ b/Sources/AXorcist/Core/AXAttributeNameConstants.swift @@ -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" +} diff --git a/Sources/AXorcist/Core/AXMiscConstants.swift b/Sources/AXorcist/Core/AXMiscConstants.swift new file mode 100644 index 0000000..a338364 --- /dev/null +++ b/Sources/AXorcist/Core/AXMiscConstants.swift @@ -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" +} diff --git a/Sources/AXorcist/Core/AXRoleNameConstants.swift b/Sources/AXorcist/Core/AXRoleNameConstants.swift new file mode 100644 index 0000000..ab8d0c8 --- /dev/null +++ b/Sources/AXorcist/Core/AXRoleNameConstants.swift @@ -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 +} diff --git a/Sources/AXorcist/Core/AccessibilityConstants.swift b/Sources/AXorcist/Core/AccessibilityConstants.swift deleted file mode 100644 index 8d47de2..0000000 --- a/Sources/AXorcist/Core/AccessibilityConstants.swift +++ /dev/null @@ -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" diff --git a/Sources/AXorcist/Core/AnyCodable.swift b/Sources/AXorcist/Core/AnyCodable.swift new file mode 100644 index 0000000..4d3b44e --- /dev/null +++ b/Sources/AXorcist/Core/AnyCodable.swift @@ -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(_ 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) + } + } +} diff --git a/Sources/AXorcist/Core/Attribute.swift b/Sources/AXorcist/Core/Attribute.swift index 0b23e4c..09e13c0 100644 --- a/Sources/AXorcist/Core/Attribute.swift +++ b/Sources/AXorcist/Core/Attribute.swift @@ -18,82 +18,82 @@ public struct Attribute { } // MARK: - General Element Attributes - public static var role: Attribute { Attribute(kAXRoleAttribute) } - public static var subrole: Attribute { Attribute(kAXSubroleAttribute) } - public static var roleDescription: Attribute { Attribute(kAXRoleDescriptionAttribute) } - public static var title: Attribute { Attribute(kAXTitleAttribute) } - public static var description: Attribute { Attribute(kAXDescriptionAttribute) } - public static var help: Attribute { Attribute(kAXHelpAttribute) } - public static var identifier: Attribute { Attribute(kAXIdentifierAttribute) } + public static var role: Attribute { Attribute(AXAttributeNames.kAXRoleAttribute) } + public static var subrole: Attribute { Attribute(AXAttributeNames.kAXSubroleAttribute) } + public static var roleDescription: Attribute { Attribute(AXAttributeNames.kAXRoleDescriptionAttribute) } + public static var title: Attribute { Attribute(AXAttributeNames.kAXTitleAttribute) } + public static var description: Attribute { Attribute(AXAttributeNames.kAXDescriptionAttribute) } + public static var help: Attribute { Attribute(AXAttributeNames.kAXHelpAttribute) } + public static var identifier: Attribute { Attribute(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 { Attribute(kAXValueAttribute) } + public static var value: Attribute { Attribute(AXAttributeNames.kAXValueAttribute) } // Example of a more specific value if known: // static var stringValue: Attribute { Attribute(kAXValueAttribute) } // MARK: - State Attributes - public static var enabled: Attribute { Attribute(kAXEnabledAttribute) } - public static var focused: Attribute { Attribute(kAXFocusedAttribute) } - public static var busy: Attribute { Attribute(kAXElementBusyAttribute) } - public static var hidden: Attribute { Attribute(kAXHiddenAttribute) } + public static var enabled: Attribute { Attribute(AXAttributeNames.kAXEnabledAttribute) } + public static var focused: Attribute { Attribute(AXAttributeNames.kAXFocusedAttribute) } + public static var busy: Attribute { Attribute(AXAttributeNames.kAXElementBusyAttribute) } + public static var hidden: Attribute { Attribute(AXAttributeNames.kAXHiddenAttribute) } // MARK: - Hierarchy Attributes - public static var parent: Attribute { Attribute(kAXParentAttribute) } + public static var parent: Attribute { Attribute(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 { Attribute(kAXMainWindowAttribute) + public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXWindowsAttribute) } + public static var mainWindow: Attribute { Attribute(AXAttributeNames.kAXMainWindowAttribute) } // Can be nil - public static var focusedWindow: Attribute { Attribute(kAXFocusedWindowAttribute) + public static var focusedWindow: Attribute { Attribute(AXAttributeNames.kAXFocusedWindowAttribute) } // Can be nil - public static var focusedElement: Attribute { Attribute(kAXFocusedUIElementAttribute) + public static var focusedElement: Attribute { Attribute(AXAttributeNames.kAXFocusedUIElementAttribute) } // Can be nil // MARK: - Application Specific Attributes // public static var enhancedUserInterface: Attribute { Attribute(kAXEnhancedUserInterfaceAttribute) } // Constant not found, commenting out - public static var frontmost: Attribute { Attribute(kAXFrontmostAttribute) } - public static var mainMenu: Attribute { Attribute(kAXMenuBarAttribute) } + public static var frontmost: Attribute { Attribute(AXAttributeNames.kAXFrontmostAttribute) } + public static var mainMenu: Attribute { Attribute(AXAttributeNames.kAXMenuBarAttribute) } // public static var hiddenApplication: Attribute { Attribute(kAXHiddenAttribute) } // Same as element hidden, but for app. Covered by .hidden // MARK: - Window Specific Attributes - public static var minimized: Attribute { Attribute(kAXMinimizedAttribute) } - public static var modal: Attribute { Attribute(kAXModalAttribute) } - public static var defaultButton: Attribute { Attribute(kAXDefaultButtonAttribute) } - public static var cancelButton: Attribute { Attribute(kAXCancelButtonAttribute) } - public static var closeButton: Attribute { Attribute(kAXCloseButtonAttribute) } - public static var zoomButton: Attribute { Attribute(kAXZoomButtonAttribute) } - public static var minimizeButton: Attribute { Attribute(kAXMinimizeButtonAttribute) } - public static var toolbarButton: Attribute { Attribute(kAXToolbarButtonAttribute) } - public static var fullScreenButton: Attribute { Attribute(kAXFullScreenButtonAttribute) + public static var minimized: Attribute { Attribute(AXAttributeNames.kAXMinimizedAttribute) } + public static var modal: Attribute { Attribute(AXAttributeNames.kAXModalAttribute) } + public static var defaultButton: Attribute { Attribute(AXAttributeNames.kAXDefaultButtonAttribute) } + public static var cancelButton: Attribute { Attribute(AXAttributeNames.kAXCancelButtonAttribute) } + public static var closeButton: Attribute { Attribute(AXAttributeNames.kAXCloseButtonAttribute) } + public static var zoomButton: Attribute { Attribute(AXAttributeNames.kAXZoomButtonAttribute) } + public static var minimizeButton: Attribute { Attribute(AXAttributeNames.kAXMinimizeButtonAttribute) } + public static var toolbarButton: Attribute { Attribute(AXAttributeNames.kAXToolbarButtonAttribute) } + public static var fullScreenButton: Attribute { Attribute(AXAttributeNames.kAXFullScreenButtonAttribute) } - public static var proxy: Attribute { Attribute(kAXProxyAttribute) } - public static var growArea: Attribute { Attribute(kAXGrowAreaAttribute) } + public static var proxy: Attribute { Attribute(AXAttributeNames.kAXProxyAttribute) } + public static var growArea: Attribute { Attribute(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 { Attribute(kAXHeaderAttribute) } + public static var header: Attribute { Attribute(AXAttributeNames.kAXHeaderAttribute) } public static var orientation: Attribute { Attribute(kAXOrientationAttribute) } // e.g., kAXVerticalOrientationValue // MARK: - Text Attributes - public static var selectedText: Attribute { Attribute(kAXSelectedTextAttribute) } - public static var selectedTextRange: Attribute { Attribute(kAXSelectedTextRangeAttribute) } - public static var numberOfCharacters: Attribute { Attribute(kAXNumberOfCharactersAttribute) } - public static var visibleCharacterRange: Attribute { Attribute(kAXVisibleCharacterRangeAttribute) + public static var selectedText: Attribute { Attribute(AXAttributeNames.kAXSelectedTextAttribute) } + public static var selectedTextRange: Attribute { Attribute(AXAttributeNames.kAXSelectedTextRangeAttribute) } + public static var numberOfCharacters: Attribute { Attribute(AXAttributeNames.kAXNumberOfCharactersAttribute) } + public static var visibleCharacterRange: Attribute { Attribute(AXAttributeNames.kAXVisibleCharacterRangeAttribute) } // Parameterized attributes are handled differently, often via functions. // static var attributedStringForRange: Attribute { Attribute(kAXAttributedStringForRangeParameterizedAttribute) } @@ -101,22 +101,22 @@ public struct Attribute { // MARK: - Scroll Area Attributes public static var horizontalScrollBar: Attribute { - Attribute(kAXHorizontalScrollBarAttribute) } + Attribute(AXAttributeNames.kAXHorizontalScrollBarAttribute) } public static var verticalScrollBar: Attribute { - Attribute(kAXVerticalScrollBarAttribute) } + Attribute(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 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 { Attribute(kAXActionDescriptionAttribute) } + public static var actionDescription: Attribute { Attribute(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 { Attribute(kAXPositionAttribute) } - public static var size: Attribute { Attribute(kAXSizeAttribute) } + public static var position: Attribute { Attribute(AXAttributeNames.kAXPositionAttribute) } + public static var size: Attribute { Attribute(AXAttributeNames.kAXSizeAttribute) } // Note: CGRect for kAXBoundsAttribute is also common if available. // For now, relying on position and size. diff --git a/Sources/AXorcist/Core/CommandModels.swift b/Sources/AXorcist/Core/CommandModels.swift new file mode 100644 index 0000000..a780a82 --- /dev/null +++ b/Sources/AXorcist/Core/CommandModels.swift @@ -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 + } +} diff --git a/Sources/AXorcist/Core/DataModels.swift b/Sources/AXorcist/Core/DataModels.swift new file mode 100644 index 0000000..f501e54 --- /dev/null +++ b/Sources/AXorcist/Core/DataModels.swift @@ -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 + } +} diff --git a/Sources/AXorcist/Core/Element+Actions.swift b/Sources/AXorcist/Core/Element+Actions.swift new file mode 100644 index 0000000..0add85e --- /dev/null +++ b/Sources/AXorcist/Core/Element+Actions.swift @@ -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: ¤tDebugLogs // This will respect the new dLog behavior in .attribute() + ) { + return actions.contains(actionName) + } + return false + } + + @MainActor + @discardableResult + public func performAction( + _ actionName: Attribute, + 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: ¤tDebugLogs + ) + 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: ¤tDebugLogs + ) + dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)") + throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error) + } + return self + } +} diff --git a/Sources/AXorcist/Core/Element+Description.swift b/Sources/AXorcist/Core/Element+Description.swift new file mode 100644 index 0000000..12458cf --- /dev/null +++ b/Sources/AXorcist/Core/Element+Description.swift @@ -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: ", ") + } +} diff --git a/Sources/AXorcist/Core/Element+Hierarchy.swift b/Sources/AXorcist/Core/Element+Hierarchy.swift index e9e13e5..35aa2b0 100644 --- a/Sources/AXorcist/Core/Element+Hierarchy.swift +++ b/Sources/AXorcist/Core/Element+Hierarchy.swift @@ -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: ¤tDebugLogs ) - 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: ¤tDebugLogs ) - 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: ¤tDebugLogs) : "Element(debug_off)" + let selfDescForLog = (isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED) ? self.briefDescription(option: .short, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) : "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.") diff --git a/Sources/AXorcist/Core/Element+ParameterizedAttributes.swift b/Sources/AXorcist/Core/Element+ParameterizedAttributes.swift new file mode 100644 index 0000000..9c5e77f --- /dev/null +++ b/Sources/AXorcist/Core/Element+ParameterizedAttributes.swift @@ -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( + _ attribute: Attribute, + 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: ¤tDebugLogs + ) + + 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 + } +} diff --git a/Sources/AXorcist/Core/Element+PathGeneration.swift b/Sources/AXorcist/Core/Element+PathGeneration.swift new file mode 100644 index 0000000..b69c218 --- /dev/null +++ b/Sources/AXorcist/Core/Element+PathGeneration.swift @@ -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 + } +} diff --git a/Sources/AXorcist/Core/Element+Properties.swift b/Sources/AXorcist/Core/Element+Properties.swift index 13c4988..37299ef 100644 --- a/Sources/AXorcist/Core/Element+Properties.swift +++ b/Sources/AXorcist/Core/Element+Properties.swift @@ -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: ¤tDebugLogs) ?? "Unknown" - let title = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) - let identifier = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) - - 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(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: false, currentDebugLogs: &logs) } - - return option == .verbose ? "<\(description)>" : description + } + + @MainActor public func domIdentifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? { + return attribute(Attribute(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) } } diff --git a/Sources/AXorcist/Core/Element.swift b/Sources/AXorcist/Core/Element.swift index 3a4fec2..43c3e5b 100644 --- a/Sources/AXorcist/Core/Element.swift +++ b/Sources/AXorcist/Core/Element.swift @@ -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(_ attribute: Attribute, 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: ¤tDebugLogs - ) { - return actions.contains(actionName) - } - return false - } - - @MainActor - @discardableResult - public func performAction( - _ actionName: Attribute, - 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: ¤tDebugLogs - ) - 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: ¤tDebugLogs - ) - 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( - _ attribute: Attribute, - 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: ¤tDebugLogs - ) - - 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: ¤tDebugLogs), - !titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr } + !titleStr.isEmpty, titleStr != AXMiscConstants.kAXNotAvailableString { return titleStr } if let valueStr: String = self.value( isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs - ) 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: ¤tDebugLogs - ), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr } + ), !descStr.isEmpty, descStr != AXMiscConstants.kAXNotAvailableString { return descStr } if let helpStr: String = self.attribute( - Attribute(kAXHelpAttribute), + Attribute(AXAttributeNames.kAXHelpAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs - ), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr } + ), !helpStr.isEmpty, helpStr != AXMiscConstants.kAXNotAvailableString { return helpStr } if let phValueStr: String = self.attribute( - Attribute(kAXPlaceholderValueAttribute), + Attribute(AXAttributeNames.kAXPlaceholderValueAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs - ), !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: ¤tDebugLogs - ), !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: ¤tDebugLogs - ) 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: ¤tDebugLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "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 - } -} diff --git a/Sources/AXorcist/Core/ElementFactories.swift b/Sources/AXorcist/Core/ElementFactories.swift new file mode 100644 index 0000000..6b9053c --- /dev/null +++ b/Sources/AXorcist/Core/ElementFactories.swift @@ -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: ¤tDebugLogs + ) 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()) +} diff --git a/Sources/AXorcist/Core/ModelEnums.swift b/Sources/AXorcist/Core/ModelEnums.swift new file mode 100644 index 0000000..d1bdc5f --- /dev/null +++ b/Sources/AXorcist/Core/ModelEnums.swift @@ -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 +} diff --git a/Sources/AXorcist/Core/Models.swift b/Sources/AXorcist/Core/Models.swift deleted file mode 100644 index 0746020..0000000 --- a/Sources/AXorcist/Core/Models.swift +++ /dev/null @@ -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(_ 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 - } -} diff --git a/Sources/AXorcist/Core/PathUtils.swift b/Sources/AXorcist/Core/PathUtils.swift index 4b5f406..dd3ecf4 100644 --- a/Sources/AXorcist/Core/PathUtils.swift +++ b/Sources/AXorcist/Core/PathUtils.swift @@ -12,4 +12,4 @@ public enum PathUtils { } return (attributeName: String(parts[0]), expectedValue: String(parts[1])) } -} \ No newline at end of file +} diff --git a/Sources/AXorcist/Core/ResponseModels.swift b/Sources/AXorcist/Core/ResponseModels.swift new file mode 100644 index 0000000..f54fb4f --- /dev/null +++ b/Sources/AXorcist/Core/ResponseModels.swift @@ -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]? +} diff --git a/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift b/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift index ef8710d..3ab25d4 100644 --- a/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift +++ b/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift @@ -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: ¤tDebugLogs) 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: ¤tDebugLogs + ) + + 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: ¤tDebugLogs))") - guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) 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: ¤tDebugLogs))") - } - - let targetElementForAction: Element - if let actualLocator = locator { - dLog("[AXorcist.handlePerformAction] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) 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: ¤tDebugLogs))." - 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: ¤tDebugLogs))") - } else { - targetElementForAction = effectiveElement - dLog("[AXorcist.handlePerformAction] No locator provided. Using current effectiveElement as target: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") - } - - dLog("[AXorcist.handlePerformAction] Element for action: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") + dLog("[AXorcist.handlePerformAction] Element for action: \(targetElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) 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: ¤tDebugLogs + ) + 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: ¤tDebugLogs - ) 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: ¤tDebugLogs ) 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: ¤tDebugLogs))") + 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: ¤tDebugLogs)) 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: ¤tDebugLogs))" - 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: ¤tDebugLogs))") - } 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: ¤tDebugLogs))") - } - dLog("[handleExtractText] Target element found: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)), attempting to extract text") var attributes: [String: AnyCodable] = [:] var extractedAnyText = false - if let valueCF = targetElementForExtract.rawAttributeValue(named: kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { + if let valueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { 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: ¤tDebugLogs) { + if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXSelectedTextAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { 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: ¤tDebugLogs) - let axElementToReturn = AXElement(attributes: attributes, path: pathArray) + let axElementToReturn = AXElement(attributes: attributes, path: pathArray) return HandlerResponse(data: axElementToReturn, error: nil, debug_logs: currentDebugLogs) } } - diff --git a/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift b/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift index ccc6747..3182dbc 100644 --- a/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift +++ b/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift @@ -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: ¤tDebugLogs @@ -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, diff --git a/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift b/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift index 5d7ed49..2054758 100644 --- a/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift +++ b/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift @@ -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) } -} \ No newline at end of file +} diff --git a/Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift b/Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift new file mode 100644 index 0000000..5596586 --- /dev/null +++ b/Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift @@ -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 { + + 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: ¤tDebugLogs + ) 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: ¤tDebugLogs))") + + guard let navigatedElement = navigateToElement( + from: effectiveElement, + pathHint: pathHint, + isDebugLoggingEnabled: isDebugLoggingEnabled, + currentDebugLogs: ¤tDebugLogs + ) 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: ¤tDebugLogs))") + } + + // Search using locator if provided + if let actualLocator = locator { + dLog("[findTargetElement] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) 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: ¤tDebugLogs + ) + 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: ¤tDebugLogs))" + 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: ¤tDebugLogs))") + 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: ¤tDebugLogs))") + return .success(effectiveElement) + } + } +} diff --git a/Sources/AXorcist/Handlers/AXorcist+QueryHandlers.swift b/Sources/AXorcist/Handlers/AXorcist+QueryHandlers.swift index b2f7cdf..17777e3 100644 --- a/Sources/AXorcist/Handlers/AXorcist+QueryHandlers.swift +++ b/Sources/AXorcist/Handlers/AXorcist+QueryHandlers.swift @@ -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: ¤tDebugLogs))" ) } - - 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: ¤tDebugLogs ) - 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: ¤tDebugLogs @@ -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: ¤tDebugLogs - ) 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: ¤tDebugLogs - ) { - 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: ¤tDebugLogs))" + "handleGetAttributes: Element found: \(foundElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)). 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: ¤tDebugLogs)). Fetching attributes: \(requestedAttributes ?? ["all"])..." - ) - var attributes = getElementAttributes( - elementToQuery, - requestedAttributes: requestedAttributes ?? [], - forMultiDefault: false, - targetRole: locator.criteria[kAXRoleAttribute], - outputFormat: outputFormat ?? .smart, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - if outputFormat == .json_string { - attributes = encodeAttributesToJSONStringRepresentation(attributes) - } - dLog( - "Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))." - ) - - 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: ¤tDebugLogs + ) + if outputFormat == .json_string { + attributes = encodeAttributesToJSONStringRepresentation(attributes) } + dLog( + "Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))." + ) + + 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: ¤tDebugLogs + ) + + 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: ¤tDebugLogs)). 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: ¤tDebugLogs - ) { - 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: ¤tDebugLogs))" - ) - - 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: ¤tDebugLogs)). 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: ¤tDebugLogs @@ -339,7 +281,7 @@ extension AXorcist { attributes: attributes, path: elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ) - + return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs) } -} \ No newline at end of file +} diff --git a/Sources/AXorcist/Handlers/QueryHandlers.swift b/Sources/AXorcist/Handlers/QueryHandlers.swift deleted file mode 100644 index 714058c..0000000 --- a/Sources/AXorcist/Handlers/QueryHandlers.swift +++ /dev/null @@ -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: ¤tDebugLogs - ) 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: ¤tDebugLogs - ) { - 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: ¤tDebugLogs - ) - 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: ¤tDebugLogs - ) - 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: ¤tDebugLogs - ) - 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: ¤tDebugLogs - ) - if outputFormat == .json_string { - attributes = encodeAttributesToJSONStringRepresentation(attributes) - } - - let elementPathArray = actualElementToQuery.generatePathArray( - upTo: appElement, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - let axElement = AXElement(attributes: attributes, path: elementPathArray) - - dLog( - "[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(actualElementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))." - ) - 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: ¤tDebugLogs - ) 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: ¤tDebugLogs - ) { - 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: ¤tDebugLogs - ) 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: ¤tDebugLogs - ) - _ = 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: ¤tDebugLogs - ) - _ = 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: ¤tDebugLogs - ) - _ = 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: ¤tDebugLogs - ) - - if outputFormat == .json_string { - attributes = encodeAttributesToJSONStringRepresentation(attributes) - } - - let elementPathArray = elementToQuery.generatePathArray( - upTo: appElement, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - 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: ¤tDebugLogs - ) 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: ¤tDebugLogs - ) { - 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: ¤tDebugLogs - ) - _ = 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: ¤tDebugLogs - ) - _ = 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: ¤tDebugLogs - ) - - if outputFormat == .json_string { - attributes = encodeAttributesToJSONStringRepresentation(attributes) - } - - let elementPathArray = elementToDescribe.generatePathArray( - upTo: appElement, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - let axElement = AXElement(attributes: attributes, path: elementPathArray) - - dLog( - "[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))." - ) - 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) - } - } -} diff --git a/Sources/AXorcist/Search/AttributeFormatter.swift b/Sources/AXorcist/Search/AttributeFormatter.swift new file mode 100644 index 0000000..325c409 --- /dev/null +++ b/Sources/AXorcist/Search/AttributeFormatter.swift @@ -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")>" } +} diff --git a/Sources/AXorcist/Search/AttributeHelpers.swift b/Sources/AXorcist/Search/AttributeHelpers.swift index 0f3c002..a18be09 100644 --- a/Sources/AXorcist/Search/AttributeHelpers.swift +++ b/Sources/AXorcist/Search/AttributeHelpers.swift @@ -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(kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) - case kAXRoleAttribute: + case AXAttributeNames.kAXPathHintAttribute: + extractedValue = element.attribute(Attribute(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: ¤tDebugLogs) } - dLog("Attribute '\(attr)' fetched via rawAttributeValue, formatted value: \(String(describing: finalValueToStore))") } if outputFormat == .smart { if let strVal = finalValueToStore as? String, - strVal.isEmpty || strVal == "" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == kAXNotAvailableString { - dLog("Smart format: Skipping attribute '\(attr)' with unhelpful value: \(strVal)") + strVal.isEmpty || strVal == "" || 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: ¤tDebugLogs) - 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 diff --git a/Sources/AXorcist/Search/AttributeMatcher.swift b/Sources/AXorcist/Search/AttributeMatcher.swift index e993c35..0af84d2 100644 --- a/Sources/AXorcist/Search/AttributeMatcher.swift +++ b/Sources/AXorcist/Search/AttributeMatcher.swift @@ -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: ¤tDebugLogs @@ -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(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: ¤tDebugLogs - ) 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(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 - } -} diff --git a/Sources/AXorcist/Search/ElementSearch.swift b/Sources/AXorcist/Search/ElementSearch.swift index 267134b..bca9d05 100644 --- a/Sources/AXorcist/Search/ElementSearch.swift +++ b/Sources/AXorcist/Search/ElementSearch.swift @@ -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: ¤tDebugLogs) { + 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: ¤tDebugLogs) { + for child in children { + let tempPathComponent = PathHintComponent(criteria: nextPathComponentCriteria) + if tempPathComponent.matches(element: child, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) { + 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: ¤tDebugLogs) { + 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: ¤tDebugLogs) { + 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: ¤tDebugLogs) } + 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: ¤tDebugLogs) { 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: ¤tDebugLogs - ) { - // 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: ¤tDebugLogs) - 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: ¤tDebugLogs -> 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: ¤tDebugLogs) { + return found } } - return (nil, internalSearchLogs) // RETURN TUPLE + return nil } @MainActor @@ -218,79 +264,36 @@ public func collectAll( elementsBeingProcessed: inout Set, 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: ¤tDebugLogs) // Pass through logs + currentDebugLogs: ¤tDebugLogs) 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: ¤tDebugLogs // Pass through logs + currentDebugLogs: ¤tDebugLogs ) } } + +// 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 diff --git a/Sources/AXorcist/Search/PathNavigator.swift b/Sources/AXorcist/Search/PathNavigator.swift new file mode 100644 index 0000000..7e292f5 --- /dev/null +++ b/Sources/AXorcist/Search/PathNavigator.swift @@ -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(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { + 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: ¤tDebugLogs) + 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: ¤tDebugLogs) { + dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)") + for child in childrenFromElementDotChildren { + let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) + if let actualValue = child.attribute(Attribute(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { + 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: ¤tDebugLogs) : "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: ¤tDebugLogs) + if let actualValue = childElement.attribute(Attribute(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { + 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: ¤tDebugLogs))", 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: ¤tDebugLogs // 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: ¤tDebugLogs))") + return currentElement +} diff --git a/Sources/AXorcist/Search/SearchCriteriaUtils.swift b/Sources/AXorcist/Search/SearchCriteriaUtils.swift new file mode 100644 index 0000000..382f516 --- /dev/null +++ b/Sources/AXorcist/Search/SearchCriteriaUtils.swift @@ -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: ¤tDebugLogs) + } +} + +// 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: ¤tDebugLogs) + 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 +} diff --git a/Sources/AXorcist/Search/SearchPathUtils.swift b/Sources/AXorcist/Search/SearchPathUtils.swift index 667a5b5..82af6df 100644 --- a/Sources/AXorcist/Search/SearchPathUtils.swift +++ b/Sources/AXorcist/Search/SearchPathUtils.swift @@ -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() 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 diff --git a/Sources/AXorcist/Search/SpecificAttributeMatchers.swift b/Sources/AXorcist/Search/SpecificAttributeMatchers.swift new file mode 100644 index 0000000..ea1178b --- /dev/null +++ b/Sources/AXorcist/Search/SpecificAttributeMatchers.swift @@ -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(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: ¤tDebugLogs + ) 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(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. + } +} diff --git a/Sources/AXorcist/Utils/ErrorUtils.swift b/Sources/AXorcist/Utils/ErrorUtils.swift new file mode 100644 index 0000000..e8480fc --- /dev/null +++ b/Sources/AXorcist/Utils/ErrorUtils.swift @@ -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))" + } +} diff --git a/Sources/AXorcist/Utils/Scanner.swift b/Sources/AXorcist/Utils/Scanner.swift index eb50023..6e5676c 100644 --- a/Sources/AXorcist/Utils/Scanner.swift +++ b/Sources/AXorcist/Utils/Scanner.swift @@ -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 { diff --git a/Sources/AXorcist/Utils/String+HelperExtensions.swift b/Sources/AXorcist/Utils/String+HelperExtensions.swift index 008a969..476490b 100644 --- a/Sources/AXorcist/Utils/String+HelperExtensions.swift +++ b/Sources/AXorcist/Utils/String+HelperExtensions.swift @@ -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 + } + } +} diff --git a/Sources/AXorcist/Utils/TextExtraction.swift b/Sources/AXorcist/Utils/TextExtraction.swift index 942f218..6f3c952 100644 --- a/Sources/AXorcist/Utils/TextExtraction.swift +++ b/Sources/AXorcist/Utils/TextExtraction.swift @@ -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 { diff --git a/Sources/AXorcist/Values/AXValueSpecificFormatter.swift b/Sources/AXorcist/Values/AXValueSpecificFormatter.swift new file mode 100644 index 0000000..57e8b6b --- /dev/null +++ b/Sources/AXorcist/Values/AXValueSpecificFormatter.swift @@ -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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : result + } + return "AXValue (\(stringFromAXValueType(.axError)))" +} + +// stringFromAXValueType is available from ValueHelpers.swift + +// axErrorToString is available from ErrorUtils.swift diff --git a/Sources/AXorcist/Values/ValueCasters.swift b/Sources/AXorcist/Values/ValueCasters.swift new file mode 100644 index 0000000..a78a621 --- /dev/null +++ b/Sources/AXorcist/Values/ValueCasters.swift @@ -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(_ 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(_ 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(_ 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(_ 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(_ 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 +} diff --git a/Sources/AXorcist/Values/ValueFormatter.swift b/Sources/AXorcist/Values/ValueFormatter.swift index 52808c2..3412198 100644 --- a/Sources/AXorcist/Values/ValueFormatter.swift +++ b/Sources/AXorcist/Values/ValueFormatter.swift @@ -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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : 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 ? "" : 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: ¤tDebugLogs) ?? "Unknown" let title = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) - + 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.. 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] { diff --git a/Sources/AXorcist/Values/ValueHelpers.swift b/Sources/AXorcist/Values/ValueHelpers.swift index be06fea..0cbf9cb 100644 --- a/Sources/AXorcist/Values/ValueHelpers.swift +++ b/Sources/AXorcist/Values/ValueHelpers.swift @@ -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( 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(_ 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(_ 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(_ 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(_ 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(_ 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 { diff --git a/Sources/AXorcist/Values/ValueParser.swift b/Sources/AXorcist/Values/ValueParser.swift index 40a8e98..7f1eb07 100644 --- a/Sources/AXorcist/Values/ValueParser.swift +++ b/Sources/AXorcist/Values/ValueParser.swift @@ -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.") diff --git a/Sources/AXorcist/Values/ValueUnwrapper.swift b/Sources/AXorcist/Values/ValueUnwrapper.swift index b96f909..6712dfd 100644 --- a/Sources/AXorcist/Values/ValueUnwrapper.swift +++ b/Sources/AXorcist/Values/ValueUnwrapper.swift @@ -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 { diff --git a/Sources/axorc/AXORCMain.swift b/Sources/axorc/AXORCMain.swift index bce9073..1bc9059 100644 --- a/Sources/axorc/AXORCMain.swift +++ b/Sources/axorc/AXORCMain.swift @@ -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) } } } diff --git a/Sources/axorc/Core/CommandExecutor.swift b/Sources/axorc/Core/CommandExecutor.swift index 4792dd9..47a7177 100644 --- a/Sources/axorc/Core/CommandExecutor.swift +++ b/Sources/axorc/Core/CommandExecutor.swift @@ -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(_ object: T) -> String? { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted @@ -272,4 +390,4 @@ struct CommandExecutor { return nil } } -} \ No newline at end of file +} diff --git a/Sources/axorc/Core/InputHandler.swift b/Sources/axorc/Core/InputHandler.swift index 3ce634a..7f44b85 100644 --- a/Sources/axorc/Core/InputHandler.swift +++ b/Sources/axorc/Core/InputHandler.swift @@ -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 , or provide JSON as a direct argument." + let error = "No input provided. Use --stdin, --file , --json , or provide JSON as a direct argument." if debug { debugLogs.append("No input method specified and no direct payload provided.") } diff --git a/Tests/AXorcistTests/AXorcistIntegrationTests.swift b/Tests/AXorcistTests/AXorcistIntegrationTests.swift index 1a97576..a921096 100644 --- a/Tests/AXorcistTests/AXorcistIntegrationTests.swift +++ b/Tests/AXorcistTests/AXorcistIntegrationTests.swift @@ -1,5 +1,5 @@ import AppKit -@testable import AXorcist +@testable import AXorcistLib import Testing import XCTest diff --git a/axorc_runner.sh b/axorc_runner.sh new file mode 100755 index 0000000..8b7c469 --- /dev/null +++ b/axorc_runner.sh @@ -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