From 3f335b489c23bfbde00ddfa273bed67ecfb9fd4d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 22 May 2025 11:00:23 +0200 Subject: [PATCH] Refactorings --- Package.swift | 8 +- Sources/AXorcist/AXorcist.swift | 224 +++++---- Sources/AXorcist/Core/Element+Hierarchy.swift | 82 +++- Sources/AXorcist/Core/Element.swift | 82 +++- Sources/AXorcist/Core/PathUtils.swift | 15 + .../Handlers/AXorcist+ActionHandlers.swift | 464 +++++------------- .../Handlers/AXorcist+BatchHandler.swift | 18 +- .../Handlers/AXorcist+CollectAllHandler.swift | 217 ++++++++ ...{PathUtils.swift => SearchPathUtils.swift} | 0 Sources/axorc/AXORCMain.swift | 7 +- Sources/axorc/Core/CommandExecutor.swift | 2 +- Sources/axorc/Core/InputHandler.swift | 12 +- Sources/axorc/Models/AXORCModels.swift | 2 +- 13 files changed, 638 insertions(+), 495 deletions(-) create mode 100644 Sources/AXorcist/Core/PathUtils.swift create mode 100644 Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift rename Sources/AXorcist/Search/{PathUtils.swift => SearchPathUtils.swift} (100%) diff --git a/Package.swift b/Package.swift index 3fb5efa..b728b83 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( .macOS(.v13) // macOS 13.0 or later ], products: [ - .library(name: "AXorcist", targets: ["AXorcist"]), + .library(name: "AXorcistLib", targets: ["AXorcistLib"]), .executable(name: "axorc", targets: ["axorc"]) // Product 'axorc' comes from target 'axorc' ], dependencies: [ @@ -18,14 +18,14 @@ let package = Package( ], targets: [ .target( - name: "AXorcist", // New library target name + name: "AXorcistLib", path: "Sources/AXorcist" // Explicit path // Sources will be inferred by SPM ), .executableTarget( name: "axorc", // Executable target name dependencies: [ - "AXorcist", + "AXorcistLib", .product(name: "ArgumentParser", package: "swift-argument-parser") // Added dependency product ], path: "Sources/axorc" // Explicit path @@ -34,7 +34,7 @@ let package = Package( .testTarget( name: "AXorcistTests", dependencies: [ - "AXorcist", // Test target depends on the library + "AXorcistLib", .product(name: "Testing", package: "swift-testing") // Added swift-testing dependency ], path: "Tests/AXorcistTests" // Explicit path diff --git a/Sources/AXorcist/AXorcist.swift b/Sources/AXorcist/AXorcist.swift index 615f50e..9179daf 100644 --- a/Sources/AXorcist/AXorcist.swift +++ b/Sources/AXorcist/AXorcist.swift @@ -66,6 +66,46 @@ public class AXorcist { // handleCollectAll method is implemented in AXorcist+ActionHandlers.swift + // 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) + } + } + + 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, @@ -73,26 +113,11 @@ public class AXorcist { isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String] ) -> Element? { - // let navigationDebugEnabled = isDebugLoggingEnabled // Create local constant // REMOVE THIS + let pathHintString = pathHint.joined(separator: ", ") + currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Entered. isDebugLoggingEnabled: \(isDebugLoggingEnabled). pathHint: [\(pathHintString)]", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) - // VERY EARLY DEBUG LOG - let pathHintString = pathHint.joined(separator: ", ") // Pre-calculate - let earlyLogMsg = AXorcist.formatDebugLogMessage( - "navigateToElement: Entered. isDebugLoggingEnabled: \\(isDebugLoggingEnabled). pathHint: [\\(pathHintString)]", // Use pre-calculated string - applicationName: nil, commandID: nil, file: #file, function: #function, line: #line - ) - currentDebugLogs.append(earlyLogMsg) - - func dLog(_ message: String) { // Removed isLoggingActive parameter - let logMessage = AXorcist.formatDebugLogMessage( - message, - applicationName: nil, - commandID: nil, - file: #file, - function: #function, - line: #line - ) - currentDebugLogs.append(logMessage) + func dLog(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: file, function: function, line: line)) } var currentElement = startElement @@ -100,101 +125,104 @@ public class AXorcist { for (index, pathComponentString) in pathHint.enumerated() { currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString - let briefDesc = currentElement.briefDescription( - option: .default, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) + let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) dLog("Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)") - let trimmedPathComponentString = pathComponentString.trimmingCharacters(in: .whitespacesAndNewlines) - dLog("Trimmed path component string: '\(trimmedPathComponentString)' (Count: \(trimmedPathComponentString.count))") - let parts = trimmedPathComponentString.split(separator: ":", maxSplits: 1) - dLog("Split parts: \(parts) (Count: \(parts.count))") - - guard parts.count == 2 else { - currentDebugLogs.append("CRITICAL_NAV_PARSE_FAILURE_MARKER") + let (attributeName, expectedValue) = PathUtils.parsePathComponent(pathComponentString) + guard !attributeName.isEmpty else { + dLog("CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty attribute name from pathComponentString '\(pathComponentString)'") return nil } - let attributeName = String(parts[0]) - let expectedValue = String(parts[1]) + var foundMatchForThisComponent = false + var newElementForNextStep: Element? = nil - var foundMatchForPathComponent = false - - // Check current element first - if let actualValueOnCurrent = currentElement.attribute( - Attribute(attributeName), - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) { - if actualValueOnCurrent == expectedValue { - let briefDesc = currentElement.briefDescription( - option: .default, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - dLog("Current element \(briefDesc) matches path component '\(attributeName):\(expectedValue)'.") - foundMatchForPathComponent = true - // No change to currentElement, this component is satisfied. - } - } - - // If current element didn't match, search children - if !foundMatchForPathComponent { - // Attempt to get children. If this element has no children, it can't match a path component. - // Note: children() can be expensive. - let children = currentElement.children( - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - - dLog("Child count: \(children?.count ?? 0)") - - // Search children for the matching attribute and value - for child in children ?? [] { - if let actualValue = child.attribute( - Attribute(attributeName), - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) { + // 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("Child (from Element.children) \(childBriefDescForLog) has '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])") if actualValue == expectedValue { - let childDesc = child.briefDescription( - option: .default, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - let matchMsg = "Matched child: \(childDesc) for '\(attributeName):\(expectedValue)'" - dLog(matchMsg) - currentElement = child - foundMatchForPathComponent = true - break // Found match for this path component, move to next in pathHint + 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.") } - if !foundMatchForPathComponent { - // All descriptive logging happens FIRST - let briefDesc = currentElement.briefDescription( - option: .default, + // 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 childBriefDescForLog = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) // Avoid for now inside loop if too verbose or risky + if let actualValue = childElement.attribute(Attribute(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) { + 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: ¤tDebugLogs + currentDebugLogs: &tempLogsForMatchCheck ) - let noMatchMsg = "Neither current element \(briefDesc) nor its children matched '\(attributeName):\(expectedValue)'. Path: \(currentPathSegmentForLog)" - dLog(noMatchMsg) - // THEN, the marker is the LAST log entry - currentDebugLogs.append("CHILD_MATCH_FAILURE_MARKER") - return nil // No match found for this path component, navigation fails + 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 } } - // If the loop completes, all path components were matched - let finalDesc = currentElement.briefDescription( - option: .default, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - dLog("Navigation successful. Final element: \(finalDesc)") + + dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") return currentElement } } diff --git a/Sources/AXorcist/Core/Element+Hierarchy.swift b/Sources/AXorcist/Core/Element+Hierarchy.swift index 1bbf369..e9e13e5 100644 --- a/Sources/AXorcist/Core/Element+Hierarchy.swift +++ b/Sources/AXorcist/Core/Element+Hierarchy.swift @@ -6,7 +6,7 @@ import Foundation extension Element { @MainActor public func children(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? { - func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } } + func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } } let elementDescription = self.briefDescription( option: .default, @@ -36,7 +36,9 @@ extension Element { currentDebugLogs: ¤tDebugLogs ) - return childCollector.finalizeResults(dLog: dLog) + let result = childCollector.finalizeResults(dLog: dLog) + dLog("Final children count from Element.children: \(result?.count ?? 0)") + return result } @MainActor @@ -45,18 +47,37 @@ extension Element { isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String] ) { - var tempLogs: [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 let directChildrenUI: [AXUIElement] = attribute( - Attribute<[AXUIElement]>.children, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: &tempLogs - ) { - currentDebugLogs.append(contentsOf: tempLogs) - collector.addChildren(from: directChildrenUI) + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(self.underlyingElement, 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)" + + 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)) + 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)) + } + } 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)) + } + } else if error == .noValue { + currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute has no value.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } else { - currentDebugLogs.append(contentsOf: tempLogs) + 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 @@ -73,6 +94,7 @@ extension Element { kAXGroupChildrenAttribute, kAXSelectedChildrenAttribute, kAXRowsAttribute, kAXColumnsAttribute, kAXTabsAttribute ] + 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( @@ -92,6 +114,7 @@ extension Element { currentDebugLogs: inout [String] ) { var tempLogs: [String] = [] + 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), @@ -99,9 +122,15 @@ extension Element { currentDebugLogs: &tempLogs ) { currentDebugLogs.append(contentsOf: tempLogs) - collector.addChildren(from: childrenUI) + 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)) + 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)) + } } 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)) } } @@ -111,21 +140,28 @@ extension Element { isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String] ) { - var tempLogs: [String] = [] - let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) - currentDebugLogs.append(contentsOf: tempLogs) + var tempLogsForRole: [String] = [] + let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForRole) + currentDebugLogs.append(contentsOf: tempLogsForRole) if currentRole == kAXApplicationRole as String { - tempLogs.removeAll() + 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: &tempLogs + currentDebugLogs: &tempLogsForWindows ) { - currentDebugLogs.append(contentsOf: tempLogs) - collector.addChildren(from: windowElementsUI) + 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)) + 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)) + } } else { - currentDebugLogs.append(contentsOf: tempLogs) + currentDebugLogs.append(contentsOf: tempLogsForWindows) + currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Attribute kAXWindowsAttribute returned nil.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } } } @@ -143,6 +179,8 @@ 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) } @@ -151,10 +189,10 @@ private struct ChildCollector { func finalizeResults(dLog: (String) -> Void) -> [Element]? { if collectedChildren.isEmpty { - dLog("No children found for element.") + dLog("ChildCollector.finalizeResults: No children found for element after all collection methods.") return nil } else { - dLog("Found \(collectedChildren.count) children.") + dLog("ChildCollector.finalizeResults: Found \(collectedChildren.count) unique children after all collection methods.") return collectedChildren } } diff --git a/Sources/AXorcist/Core/Element.swift b/Sources/AXorcist/Core/Element.swift index 422cfcc..a35fa41 100644 --- a/Sources/AXorcist/Core/Element.swift +++ b/Sources/AXorcist/Core/Element.swift @@ -26,13 +26,81 @@ public struct Element: Equatable, Hashable { @MainActor public func attribute(_ attribute: Attribute, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> T? { - // axValue is from ValueHelpers.swift and now expects logging parameters - return axValue( - of: self.underlyingElement, - attr: attribute.rawValue, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) as T? + func dLog(_ message: String) { if isDebugLoggingEnabled { 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)") + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(self.underlyingElement, attribute.rawValue as CFString, &value) + if error == .success { + if let cfArray = value, CFGetTypeID(cfArray) == CFArrayGetTypeID() { + if let axElements = cfArray as? [AXUIElement] { + dLog("Element.attribute: Successfully fetched and cast \(axElements.count) AXUIElements for '\(attribute.rawValue)'.") + return axElements as? T // This cast should succeed due to the T.self check + } else { + dLog("Element.attribute: CFArray for '\(attribute.rawValue)' failed to cast to [AXUIElement].") + } + } else if value != nil { + dLog("Element.attribute: Value for '\(attribute.rawValue)' was not a CFArray. TypeID: \(CFGetTypeID(value!))") + } else { + dLog("Element.attribute: Value for '\(attribute.rawValue)' was nil despite .success.") + } + } else if error == .noValue { + dLog("Element.attribute: Attribute '\(attribute.rawValue)' has no value.") + } else { + dLog("Element.attribute: Error fetching '\(attribute.rawValue)': \(error.rawValue)") + } + return nil // Return nil if any step above failed for [AXUIElement] + } else { + // RESTORED: Minimal survival path for common types, otherwise nil. + // Full ValueUnwrapper logic is still TODO. + dLog("Element.attribute: Using basic CFTypeRef conversion for T = \(String(describing: T.self)), Attribute: \(attribute.rawValue).") + var value: CFTypeRef? + let error = AXUIElementCopyAttributeValue(self.underlyingElement, attribute.rawValue as CFString, &value) + + if error != .success { + if error != .noValue { // Don't log for .noValue, it's common + dLog("Element.attribute: Error \(error.rawValue) fetching '\(attribute.rawValue)' for basic conversion.") + } + return nil + } + + guard let unwrappedValue = value else { + dLog("Element.attribute: Value was nil for '\(attribute.rawValue)' after fetch for basic conversion.") + return nil + } + + // Basic unwrapping for common types + if T.self == String.self { + if CFGetTypeID(unwrappedValue) == CFStringGetTypeID() { + return (unwrappedValue as! CFString) as String as? T + } + } else if T.self == Bool.self { + if CFGetTypeID(unwrappedValue) == CFBooleanGetTypeID() { + return CFBooleanGetValue(unwrappedValue as! CFBoolean) as? T + } + } else if T.self == Int.self { + if CFGetTypeID(unwrappedValue) == CFNumberGetTypeID() { + var intValue: Int = 0 + 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() { + 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).") + return directCast + } + + dLog("Element.attribute: Basic conversion FAILED for T = \(String(describing: T.self)), Attribute: \(attribute.rawValue). Value type: \(CFGetTypeID(unwrappedValue))") + return nil + } } // Method to get the raw CFTypeRef? for an attribute diff --git a/Sources/AXorcist/Core/PathUtils.swift b/Sources/AXorcist/Core/PathUtils.swift new file mode 100644 index 0000000..4b5f406 --- /dev/null +++ b/Sources/AXorcist/Core/PathUtils.swift @@ -0,0 +1,15 @@ +import Foundation + +public enum PathUtils { + public static func parsePathComponent(_ pathComponent: String) -> (attributeName: String, expectedValue: String) { + let trimmedPathComponentString = pathComponent.trimmingCharacters(in: .whitespacesAndNewlines) + let parts = trimmedPathComponentString.split(separator: ":", maxSplits: 1) + + guard parts.count == 2 else { + // AXorcist's navigateToElement should handle this, e.g. by logging a CRITICAL_NAV_PARSE_FAILURE_MARKER + // and returning nil from navigateToElement if attributeName is empty. + return (attributeName: "", expectedValue: "") + } + return (attributeName: String(parts[0]), expectedValue: String(parts[1])) + } +} \ No newline at end of file diff --git a/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift b/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift index d679dba..52f8569 100644 --- a/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift +++ b/Sources/AXorcist/Handlers/AXorcist+ActionHandlers.swift @@ -10,7 +10,7 @@ extension AXorcist { @MainActor public func handlePerformAction( for appIdentifierOrNil: String? = nil, - locator: Locator, + locator: Locator?, pathHint: [String]? = nil, actionName: String, actionValue: AnyCodable?, @@ -21,7 +21,7 @@ extension AXorcist { func dLog(_ message: String) { if isDebugLoggingEnabled { - currentDebugLogs.append(message) + currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line)) } } @@ -30,52 +30,56 @@ extension AXorcist { guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) else { let error = "[AXorcist.handlePerformAction] Failed to get application element for identifier: \(appIdentifier)" - dLog(error) + currentDebugLogs.append(error) return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs) } var effectiveElement = appElement if let pathHint = pathHint, !pathHint.isEmpty { - dLog("[AXorcist.handlePerformAction] Navigating with path_hint: \(pathHint.joined(separator: " -> "))") + 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 { - // Check if the last log entry contains the critical navigation parse failure marker BEFORE adding debug logs let lastLogBeforeDebug = currentDebugLogs.last let error: String - if let lastLog = lastLogBeforeDebug, lastLog == "CRITICAL_NAV_PARSE_FAILURE_MARKER" { - error = "Navigation parsing failed: Critical marker found." - } else if let lastLog = lastLogBeforeDebug, lastLog == "CHILD_MATCH_FAILURE_MARKER" { - error = "Navigation child match failed: Child match marker found." + 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: " -> "))" } - // ADD DEBUG LOGGING BLOCK FOR MARKER CHECK if isDebugLoggingEnabled { if let actualLastLog = lastLogBeforeDebug { - dLog("[MARKER_CHECK] Checked lastLog: '\(actualLastLog)' -> Error: '\(error)'") + dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(error)'. LastLog: '\(actualLastLog)'") } else { dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(error)'") } } - // END OF ADDED LOGGING BLOCK - dLog(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))") } - dLog("[AXorcist.handlePerformAction] Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") - guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, depth: 0, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) else { - let error = "[AXorcist.handlePerformAction] Failed to find element with locator: \(locator)" - dLog(error) - return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs) + 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)") + guard let foundElement = search(element: effectiveElement, locator: actualLocator, requireAction: actualLocator.requireAction, depth: 0, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) else { + let error = "[AXorcist.handlePerformAction] Failed to find element with locator: \(actualLocator) starting from \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))" + 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] Found element: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") + + dLog("[AXorcist.handlePerformAction] Element for action: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") if let actionValue = actionValue { - // Attempt to get a string representation of actionValue.value for logging - // This is a basic attempt; complex types might not log well. let valueDescription = String(describing: actionValue.value) dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)' with value: \(valueDescription)") } else { @@ -83,49 +87,48 @@ extension AXorcist { } var errorMessage: String? - var axStatus: AXError = .success // Initialize to success + var axStatus: AXError = .success switch actionName.lowercased() { case "press": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXPressAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPressAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform press action: \(axErrorToString(axStatus))" } case "increment": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXIncrementAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXIncrementAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform increment action: \(axErrorToString(axStatus))" } case "decrement": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXDecrementAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXDecrementAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform decrement action: \(axErrorToString(axStatus))" } case "showmenu": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXShowMenuAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXShowMenuAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform showmenu action: \(axErrorToString(axStatus))" } case "pick": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXPickAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPickAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform pick action: \(axErrorToString(axStatus))" } case "cancel": - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXCancelAction as CFString) + axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXCancelAction as CFString) if axStatus != .success { errorMessage = "[AXorcist.handlePerformAction] Failed to perform cancel action: \(axErrorToString(axStatus))" } default: if actionName.hasPrefix("AX") { - axStatus = AXUIElementPerformAction(foundElement.underlyingElement, actionName as CFString) + 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? - // Convert basic Swift types to CFTypeRef for setting attributes switch actionValue.value { case let stringValue as String: cfValue = stringValue as CFString @@ -137,13 +140,9 @@ extension AXorcist { case let doubleValue as Double: var number = doubleValue cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number) - // TODO: Consider other CFNumber types if necessary (CGFloat, etc.) - // TODO: Consider CFArray, CFDictionary if complex values are needed. default: - // For other types, attempt a direct cast if possible, or log/error. - // This is a simplification; robust conversion is more involved. - if CFGetTypeID(actionValue.value as AnyObject) != 0 { // Basic check if it *might* be a CFType - cfValue = actionValue.value as AnyObject // bridge from Any to AnyObject then to CFTypeRef + 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." @@ -152,21 +151,21 @@ extension AXorcist { } if errorMessage == nil, let finalCFValue = cfValue { - axStatus = AXUIElementSetAttributeValue(foundElement.underlyingElement, actionName as CFString, finalCFValue) + 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 { // cfValue was nil, means conversion failed earlier but wasn't caught by the default error + } else if errorMessage == nil { errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type." } } else { - errorMessage = "[AXorcist.handlePerformAction] Unknown action '\(actionName)' and no action_value provided to interpret as an attribute." + errorMessage = "[AXorcist.handlePerformAction] Attribute action '\(actionName)' requires an action_value, but none was provided." } } } if let currentErrorMessage = errorMessage { - dLog(currentErrorMessage) + currentDebugLogs.append(currentErrorMessage) return HandlerResponse(data: nil, error: currentErrorMessage, debug_logs: currentDebugLogs) } @@ -177,374 +176,131 @@ extension AXorcist { @MainActor public func handleExtractText( for appIdentifierOrNil: String? = nil, - locator: Locator, + locator: Locator?, pathHint: [String]? = nil, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String] ) -> HandlerResponse { func dLog(_ message: String) { if isDebugLoggingEnabled { - currentDebugLogs.append("[handleExtractText] \(message)") + currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line)) } } let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue - dLog("Starting text extraction for app: \(appIdentifier)") + dLog("[handleExtractText] Starting text extraction for app: \(appIdentifier)") guard let appElement = applicationElement( for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs ) else { - let errorMessage = "Failed to get application element for \(appIdentifier)" - dLog(errorMessage) + 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("Navigating to element using path hint: \(pathHint.joined(separator: " -> "))") + dLog("[handleExtractText] Navigating to element using path hint: \(pathHint.joined(separator: " -> "))") guard let navigatedElement = navigateToElement( from: appElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs ) else { - // Check if the last log entry contains the critical navigation parse failure marker BEFORE adding debug logs let lastLogBeforeDebug = currentDebugLogs.last let errorMessage: String - if let lastLog = lastLogBeforeDebug, lastLog == "CRITICAL_NAV_PARSE_FAILURE_MARKER" { - errorMessage = "Navigation parsing failed: Critical marker found." - } else if let lastLog = lastLogBeforeDebug, lastLog == "CHILD_MATCH_FAILURE_MARKER" { - errorMessage = "Navigation child match failed: Child match marker found." + 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 = "Failed to navigate to element using path hint: \(pathHint.joined(separator: " -> "))" + errorMessage = "[handleExtractText] Failed to navigate to element using path hint: \(pathHint.joined(separator: " -> "))" } - // ADD DEBUG LOGGING BLOCK FOR MARKER CHECK if isDebugLoggingEnabled { if let actualLastLog = lastLogBeforeDebug { - dLog("[MARKER_CHECK] Checked lastLog: '\(actualLastLog)' -> Error: '\(errorMessage)'") + dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(errorMessage)'. LastLog: '\(actualLastLog)'") } else { dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(errorMessage)'") } } - // END OF ADDED LOGGING BLOCK - dLog(errorMessage) + 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))") } - dLog("Searching for target element with locator: \(locator)") - // Assuming DEFAULT_MAX_DEPTH_SEARCH is defined elsewhere, e.g., in AXConstants.swift or similar. - // If not, replace with a sensible default like 10. - guard let foundElement = search( - element: effectiveElement, - locator: locator, - requireAction: locator.requireAction, - maxDepth: DEFAULT_MAX_DEPTH_SEARCH, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) else { - let errorMessage = "Target element not found for locator: \(locator)" - dLog(errorMessage) - return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs) - } - - dLog( - "Target element found: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)), attempting to extract text" - ) - var attributes: [String: AnyCodable] = [:] - var extractedValueText: String? - var extractedSelectedText: String? - - var cfValue: CFTypeRef? - if AXUIElementCopyAttributeValue(foundElement.underlyingElement, kAXValueAttribute as CFString, &cfValue) == - .success, let value = cfValue { - if CFGetTypeID(value) == CFStringGetTypeID() { - extractedValueText = (value as! CFString) as String - if let extractedValueText = extractedValueText, !extractedValueText.isEmpty { - attributes["extractedValue"] = AnyCodable(extractedValueText) - dLog( - "Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))..." - ) - } else { - dLog("kAXValueAttribute was empty or not a string.") - } - } else { - dLog("kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(value))") - } - } else { - dLog("Failed to get kAXValueAttribute or it was nil.") - } - - cfValue = nil // Reset for next attribute - if AXUIElementCopyAttributeValue( - foundElement.underlyingElement, - kAXSelectedTextAttribute as CFString, - &cfValue - ) == .success, let selectedValue = cfValue { - if CFGetTypeID(selectedValue) == CFStringGetTypeID() { - extractedSelectedText = (selectedValue as! CFString) as String - if let extractedSelectedText = extractedSelectedText, !extractedSelectedText.isEmpty { - attributes["extractedSelectedText"] = AnyCodable(extractedSelectedText) - dLog( - "Extracted selected text from kAXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))..." - ) - } else { - dLog("kAXSelectedTextAttribute was empty or not a string.") - } - } else { - dLog("kAXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValue))") - } - } else { - dLog("Failed to get kAXSelectedTextAttribute or it was nil.") - } - - - if attributes.isEmpty { - dLog( - "Warning: No text could be extracted from the element via kAXValueAttribute or kAXSelectedTextAttribute." - ) - // It's not an error, just means no text content via these primary attributes. - // Other attributes might still be relevant, so we return the element. - } - - let elementPathArray = foundElement.generatePathArray( - upTo: appElement, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: ¤tDebugLogs - ) - // Include any other relevant attributes if needed, for now just the extracted text - let axElement = AXElement(attributes: attributes, path: elementPathArray) - - dLog("Text extraction process completed.") - return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs) - } - - @MainActor - public func handleCollectAll( - for appIdentifierOrNil: String?, - locator: Locator?, - pathHint: [String]?, - maxDepth: Int?, // This is the input from the command - requestedAttributes: [String]?, - outputFormat: OutputFormat?, - commandId: String?, - isDebugLoggingEnabled: Bool, - currentDebugLogs: [String] // No longer inout, logs from caller - ) -> String { - self.recursiveCallDebugLogs.removeAll() - self.recursiveCallDebugLogs.append(contentsOf: currentDebugLogs) // Incorporate initial logs - - let effectiveCommandId = commandId ?? "collectAll_internal_id_error" - - // Centralized JSON encoding helper for CollectAllOutput - func encode(_ output: CollectAllOutput) -> String { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - do { - let jsonData = try encoder.encode(output) - return String(data: jsonData, encoding: .utf8) ?? "{\"error\":\"Failed to encode CollectAllOutput to string (fallback)\"}" // Minimal fallback - } catch { - let errorMsgForLog = "Exception encoding CollectAllOutput: \\(error.localizedDescription)" - self.recursiveCallDebugLogs.append(errorMsgForLog) // Log it - // Extremely simplified fallback JSON for catastrophic failure of encoder.encode(output) - return "{\"command_id\":\"Unknown\", \"success\":false, \"command\":\"Unknown\", \"error_message\":\"Catastrophic JSON encoding failure for CollectAllOutput. Original error logged.\", \"collected_elements\":[], \"debug_logs\":[\"Catastrophic JSON encoding failure as well.\"]}" - } - } - - func dLog( - _ message: String, - _ file: String = #file, - _ function: String = #function, - _ line: Int = #line - ) { - let logMessage = AXorcist.formatDebugLogMessage( - message, - applicationName: appIdentifierOrNil, - commandID: effectiveCommandId, - file: file, - function: function, - line: line - ) - self.recursiveCallDebugLogs.append(logMessage) - } - - let appNameForLog = appIdentifierOrNil ?? "N/A" - let locatorDesc = String(describing: locator) - let pathHintDesc = String(describing: pathHint) - let maxDepthDesc = String(describing: maxDepth) - dLog( - "[AXorcist.handleCollectAll] Starting. App: \\(appNameForLog), Locator: \\(locatorDesc), PathHint: \\(pathHintDesc), MaxDepth: \\(maxDepthDesc)" - ) - - let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXorcist.defaultMaxDepthCollectAll - let attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch - let effectiveOutputFormat = outputFormat ?? .smart - - dLog( - "Effective recursionDepthLimit: \\(recursionDepthLimit), attributesToFetch: \\(attributesToFetch.count) items, effectiveOutputFormat: \\(effectiveOutputFormat.rawValue)" - ) - - let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue - dLog("Using app identifier: \\(appIdentifier)") - - guard let appElement = applicationElement( - for: appIdentifier, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: &self.recursiveCallDebugLogs - ) else { - let errorMsg = "Failed to get app element for identifier: \\(appIdentifier)" - dLog(errorMsg) // errorMsg is already added to recursiveCallDebugLogs by dLog - return encode(CollectAllOutput( - command_id: effectiveCommandId, - success: false, - command: "collectAll", - collected_elements: [], - app_bundle_id: appIdentifier, - debug_logs: self.recursiveCallDebugLogs - )) - } - - var startElement: Element - if let hint = pathHint, !hint.isEmpty { - let pathHintString = hint.joined(separator: " -> ") - dLog("Navigating to path hint: \\(pathHintString)") - guard let navigatedElement = navigateToElement( - from: appElement, - pathHint: hint, + 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)") + guard let foundElement = search( + element: effectiveElement, + locator: actualLocator, + requireAction: nil, + depth: 0, + maxDepth: DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: &self.recursiveCallDebugLogs + currentDebugLogs: ¤tDebugLogs ) else { - let lastLogBeforeError = self.recursiveCallDebugLogs.last - var errorMsg = "Failed to navigate to path: \\(pathHintString)" // Use pre-calculated pathHintString - if let lastLog = lastLogBeforeError, lastLog == "CRITICAL_NAV_PARSE_FAILURE_MARKER" { - errorMsg = "Navigation parsing failed: Critical marker found." - } else if let lastLog = lastLogBeforeError, lastLog == "CHILD_MATCH_FAILURE_MARKER" { - errorMsg = "Navigation child match failed: Child match marker found." - } - dLog(errorMsg) // Log the specific navigation error reason - return encode(CollectAllOutput( - command_id: effectiveCommandId, - success: false, - command: "collectAll", - collected_elements: [], - app_bundle_id: appIdentifier, - debug_logs: self.recursiveCallDebugLogs - )) + let errorMessage = "[handleExtractText] Target element not found for locator: \(actualLocator) starting from \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))" + currentDebugLogs.append(errorMessage) + return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs) } - startElement = navigatedElement + targetElementForExtract = foundElement + dLog("[handleExtractText] Found element via locator for text extraction: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))") } else { - dLog("Using app element as start element") - startElement = appElement + 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 loc = locator { - dLog("Locator provided. Searching for element from current startElement: \\(startElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) with locator: \\(loc.description)") - if let locatedStartElement = search(element: startElement, locator: loc, requireAction: loc.requireAction, depth: 0, maxDepth: Self.defaultMaxDepthSearch, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs) { - dLog("Locator found element: \\(locatedStartElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)). This will be the root for collectAll recursion.") - startElement = locatedStartElement - } else { - let errorMsg = "Failed to find element with provided locator: \\(loc.description). Cannot start collectAll." - dLog(errorMsg) - return encode(CollectAllOutput( - command_id: effectiveCommandId, - success: false, - command: "collectAll", - collected_elements: [], - app_bundle_id: appIdentifier, - debug_logs: self.recursiveCallDebugLogs - )) - } - } - - var collectedAXElements: [AXElement] = [] - var collectRecursively: ((AXUIElement, Int) -> Void)! - collectRecursively = { axUIElement, currentDepth in - if currentDepth > recursionDepthLimit { - dLog( - "Reached recursionDepthLimit (\\(recursionDepthLimit)) at element \\(Element(axUIElement).briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)), stopping recursion for this branch." - ) - return - } - - let currentElement = Element(axUIElement) - dLog("Collecting element \\(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \\(currentDepth)") - - let fetchedAttrs = getElementAttributes( - currentElement, - requestedAttributes: attributesToFetch, - forMultiDefault: true, - targetRole: nil, - outputFormat: effectiveOutputFormat, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: &self.recursiveCallDebugLogs - ) - - let elementPath = currentElement.generatePathArray( - upTo: appElement, - isDebugLoggingEnabled: isDebugLoggingEnabled, - currentDebugLogs: &self.recursiveCallDebugLogs - ) - let axElement = AXElement(attributes: fetchedAttrs, path: elementPath) - collectedAXElements.append(axElement) - - var childrenRef: CFTypeRef? - let childrenResult = AXUIElementCopyAttributeValue( - axUIElement, - kAXChildrenAttribute as CFString, - &childrenRef - ) - - if childrenResult == .success, let children = childrenRef as? [AXUIElement] { - dLog( - "Element \\(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) has \\(children.count) children at depth \\(currentDepth). Recursing." - ) - for childElement in children { - collectRecursively(childElement, currentDepth + 1) + if let valueCF = targetElementForExtract.rawAttributeValue(named: 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))...") + } else { + dLog("[handleExtractText] kAXValueAttribute was empty or not a string.") } - } else if childrenResult != .success { - dLog( - "Failed to get children for element \\(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)): \\(axErrorToString(childrenResult))" - ) } else { - dLog( - "No children found for element \\(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \\(currentDepth)" - ) + dLog("[handleExtractText] kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(valueCF))") } + } else { + dLog("[handleExtractText] kAXValueAttribute not found or nil.") } - dLog( - "Starting recursive collection from start element: \\(startElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs))" - ) - collectRecursively(startElement.underlyingElement, 0) + if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: 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))...") + } else { + dLog("[handleExtractText] kAXSelectedTextAttribute was empty or not a string.") + } + } else { + dLog("[handleExtractText] kAXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValueCF))") + } + } else { + dLog("[handleExtractText] kAXSelectedTextAttribute not found or nil.") + } + + if !extractedAnyText { + dLog("[handleExtractText] No text could be extracted from kAXValue or kAXSelectedText for element.") + } - dLog("Collection complete. Found \\(collectedAXElements.count) elements.") - - return encode(CollectAllOutput( - command_id: effectiveCommandId, - success: true, - command: "collectAll", - collected_elements: collectedAXElements, - app_bundle_id: appIdentifier, - debug_logs: self.recursiveCallDebugLogs - )) + let pathArray = targetElementForExtract.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) + let axElementToReturn = AXElement(attributes: attributes, path: pathArray) + return HandlerResponse(data: axElementToReturn, error: nil, debug_logs: currentDebugLogs) } - - // Helper to encode CollectAllOutput, ensure it exists in AXorcist or is added. - // If it doesn't exist, this edit will require it. - // For now, assuming it's available. - // private func encodeOutputToJSON(output: CollectAllOutput) -> String { - // let encoder = JSONEncoder() - // encoder.outputFormatting = .prettyPrinted - // do { - // let data = try encoder.encode(output) - // return String(data: data, encoding: .utf8) ?? "{\\"error\\":\\"Failed to encode CollectAllOutput to JSON string\\"}" - // } catch { - // return "{\\"error\\":\\"Exception encoding CollectAllOutput: \\(error.localizedDescription)\\"}" - // } - // } } diff --git a/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift b/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift index 4be471b..aa0e210 100644 --- a/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift +++ b/Sources/AXorcist/Handlers/AXorcist+BatchHandler.swift @@ -103,26 +103,34 @@ extension AXorcist { ) case .performAction: - guard let locator = subCommandEnvelope.locator else { - let errorMsg = "Locator missing for performAction in batch (sub-command ID: \(subCmdID))" + // Check if either locator or path_hint is provided + let hasLocator = subCommandEnvelope.locator != nil + let hasPathHint = subCommandEnvelope.path_hint != nil && !(subCommandEnvelope.path_hint?.isEmpty ?? true) + + guard hasLocator || hasPathHint else { + let errorMsg = "Locator or path_hint missing for performAction in batch (sub-command ID: \(subCmdID))" dLog(errorMsg, subCommandID: subCmdID) 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) subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil) break } + + // 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( for: subCommandEnvelope.application, - locator: locator, - pathHint: subCommandEnvelope.path_hint, + locator: subCommandEnvelope.locator, // Pass along, might be nil + pathHint: subCommandEnvelope.path_hint, // Pass along, might be nil or empty actionName: actionName, actionValue: subCommandEnvelope.action_value, maxDepth: subCommandEnvelope.max_elements, - // Added maxDepth, though performAction doesn't currently use it directly, for consistency isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs ) diff --git a/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift b/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift new file mode 100644 index 0000000..b54d609 --- /dev/null +++ b/Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift @@ -0,0 +1,217 @@ +// AXorcist+CollectAllHandler.swift - CollectAll operation handler + +import AppKit +import ApplicationServices +import Foundation + +// MARK: - CollectAll Handler Extension +extension AXorcist { + + private func encode(_ output: CollectAllOutput) -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + do { + let jsonData = try encoder.encode(output) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\":\"Failed to encode CollectAllOutput to string (fallback)\"}" + } catch { + let errorMsgForLog = "Exception encoding CollectAllOutput: \(error.localizedDescription)" + self.recursiveCallDebugLogs.append(errorMsgForLog) + return "{\"command_id\":\"Unknown\", \"success\":false, \"command\":\"Unknown\", \"error_message\":\"Catastrophic JSON encoding failure for CollectAllOutput. Original error logged.\", \"collected_elements\":[], \"debug_logs\":[\"Catastrophic JSON encoding failure as well.\"]}" + } + } + + @MainActor + public func handleCollectAll( + for appIdentifierOrNil: String?, + locator: Locator?, + pathHint: [String]?, + maxDepth: Int?, + requestedAttributes: [String]?, + outputFormat: OutputFormat?, + commandId: String?, + isDebugLoggingEnabled: Bool, + currentDebugLogs: [String] + ) -> String { + self.recursiveCallDebugLogs.removeAll() + self.recursiveCallDebugLogs.append(contentsOf: currentDebugLogs) + + let effectiveCommandId = commandId ?? "collectAll_internal_id_error" + + func dLog( + _ message: String, + _ file: String = #file, + _ function: String = #function, + _ line: Int = #line + ) { + let logMessage = AXorcist.formatDebugLogMessage( + message, + applicationName: appIdentifierOrNil, + commandID: effectiveCommandId, + file: file, + function: function, + line: line + ) + self.recursiveCallDebugLogs.append(logMessage) + } + + let appNameForLog = appIdentifierOrNil ?? "N/A" + let locatorDesc = locator != nil ? String(describing: locator!.criteria) : "nil" + let pathHintDesc = String(describing: pathHint) + let maxDepthDesc = String(describing: maxDepth) + dLog( + "[AXorcist.handleCollectAll] Starting. App: \(appNameForLog), Locator: \(locatorDesc), PathHint: \(pathHintDesc), MaxDepth: \(maxDepthDesc)" + ) + + let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXorcist.defaultMaxDepthCollectAll + let attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch + let effectiveOutputFormat = outputFormat ?? .smart + + dLog( + "Effective recursionDepthLimit: \(recursionDepthLimit), attributesToFetch: \(attributesToFetch.count) items, effectiveOutputFormat: \(effectiveOutputFormat.rawValue)" + ) + + let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue + dLog("Using app identifier: \(appIdentifier)") + + guard let appElement = applicationElement( + for: appIdentifier, + isDebugLoggingEnabled: isDebugLoggingEnabled, + currentDebugLogs: &self.recursiveCallDebugLogs + ) else { + let errorMsg = "Failed to get app element for identifier: \(appIdentifier)" + dLog(errorMsg) + return encode(CollectAllOutput( + command_id: effectiveCommandId, + success: false, + command: "collectAll", + collected_elements: [], + app_bundle_id: appIdentifier, + debug_logs: self.recursiveCallDebugLogs + )) + } + + var startElement: Element + if let hint = pathHint, !hint.isEmpty { + let pathHintString = hint.joined(separator: " -> ") + dLog("Navigating to path hint: \(pathHintString)") + guard let navigatedElement = navigateToElement( + from: appElement, + pathHint: hint, + isDebugLoggingEnabled: isDebugLoggingEnabled, + currentDebugLogs: &self.recursiveCallDebugLogs + ) else { + let lastLogBeforeError = self.recursiveCallDebugLogs.last + var errorMsg = "Failed to navigate to path: \(pathHintString)" + if let lastLog = lastLogBeforeError, lastLog == "CRITICAL_NAV_PARSE_FAILURE_MARKER" { + errorMsg = "Navigation parsing failed: Critical marker found." + } else if let lastLog = lastLogBeforeError, lastLog == "CHILD_MATCH_FAILURE_MARKER" { + errorMsg = "Navigation child match failed: Child match marker found." + } + dLog(errorMsg) + return encode(CollectAllOutput( + command_id: effectiveCommandId, + success: false, + command: "collectAll", + collected_elements: [], + app_bundle_id: appIdentifier, + debug_logs: self.recursiveCallDebugLogs + )) + } + startElement = navigatedElement + } else { + dLog("Using app element as start element") + startElement = appElement + } + + if let loc = locator { + dLog("Locator provided. Searching for element from current startElement: \(startElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) with locator criteria: \(String(describing: loc.criteria))") + if let locatedStartElement = search(element: startElement, locator: loc, requireAction: loc.requireAction, depth: 0, maxDepth: Self.defaultMaxDepthSearch, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs) { + dLog("Locator found element: \(locatedStartElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)). This will be the root for collectAll recursion.") + startElement = locatedStartElement + } else { + let errorMsg = "Failed to find element with provided locator criteria: \(String(describing: loc.criteria)). Cannot start collectAll." + dLog(errorMsg) + return encode(CollectAllOutput( + command_id: effectiveCommandId, + success: false, + command: "collectAll", + collected_elements: [], + app_bundle_id: appIdentifier, + debug_logs: self.recursiveCallDebugLogs + )) + } + } + + var collectedAXElements: [AXElement] = [] + var collectRecursively: ((AXUIElement, Int) -> Void)! + collectRecursively = { axUIElement, currentDepth in + if currentDepth > recursionDepthLimit { + dLog( + "Reached recursionDepthLimit (\(recursionDepthLimit)) at element \(Element(axUIElement).briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)), stopping recursion for this branch." + ) + return + } + + let currentElement = Element(axUIElement) + dLog("Collecting element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)") + + let fetchedAttrs = getElementAttributes( + currentElement, + requestedAttributes: attributesToFetch, + forMultiDefault: true, + targetRole: nil, + outputFormat: effectiveOutputFormat, + isDebugLoggingEnabled: isDebugLoggingEnabled, + currentDebugLogs: &self.recursiveCallDebugLogs + ) + + let elementPath = currentElement.generatePathArray( + upTo: appElement, + isDebugLoggingEnabled: isDebugLoggingEnabled, + currentDebugLogs: &self.recursiveCallDebugLogs + ) + let axElement = AXElement(attributes: fetchedAttrs, path: elementPath) + collectedAXElements.append(axElement) + + var childrenRef: CFTypeRef? + let childrenResult = AXUIElementCopyAttributeValue( + axUIElement, + kAXChildrenAttribute as CFString, + &childrenRef + ) + + if childrenResult == .success, let children = childrenRef as? [AXUIElement] { + dLog( + "Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) has \(children.count) children at depth \(currentDepth). Recursing." + ) + for childElement in children { + collectRecursively(childElement, currentDepth + 1) + } + } else if childrenResult != .success { + dLog( + "Failed to get children for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)): \(axErrorToString(childrenResult))" + ) + } else { + dLog( + "No children found for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)" + ) + } + } + + dLog( + "Starting recursive collection from start element: \(startElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs))" + ) + collectRecursively(startElement.underlyingElement, 0) + + dLog("Collection complete. Found \(collectedAXElements.count) elements.") + + return encode(CollectAllOutput( + command_id: effectiveCommandId, + success: true, + command: "collectAll", + collected_elements: collectedAXElements, + app_bundle_id: appIdentifier, + debug_logs: self.recursiveCallDebugLogs + )) + } +} \ No newline at end of file diff --git a/Sources/AXorcist/Search/PathUtils.swift b/Sources/AXorcist/Search/SearchPathUtils.swift similarity index 100% rename from Sources/AXorcist/Search/PathUtils.swift rename to Sources/AXorcist/Search/SearchPathUtils.swift diff --git a/Sources/axorc/AXORCMain.swift b/Sources/axorc/AXORCMain.swift index acd996f..bce9073 100644 --- a/Sources/axorc/AXORCMain.swift +++ b/Sources/axorc/AXORCMain.swift @@ -1,7 +1,7 @@ // AXORCMain.swift - Main entry point for AXORC CLI import ArgumentParser -import AXorcist +import AXorcistLib import Foundation @main @@ -79,6 +79,11 @@ struct AXORCCommand: AsyncParsableCommand { return } + if debug { + localDebugLogs.append("AXORCMain: jsonString before decode: [\(jsonString)]") + localDebugLogs.append("AXORCMain: jsonData.count before decode: \(jsonData.count)") + } + do { let command = try JSONDecoder().decode(CommandEnvelope.self, from: jsonData) diff --git a/Sources/axorc/Core/CommandExecutor.swift b/Sources/axorc/Core/CommandExecutor.swift index 5ab83d6..23661b8 100644 --- a/Sources/axorc/Core/CommandExecutor.swift +++ b/Sources/axorc/Core/CommandExecutor.swift @@ -1,6 +1,6 @@ // CommandExecutor.swift - Executes AXORC commands -import AXorcist +import AXorcistLib import Foundation struct CommandExecutor { diff --git a/Sources/axorc/Core/InputHandler.swift b/Sources/axorc/Core/InputHandler.swift index 223693b..3ce634a 100644 --- a/Sources/axorc/Core/InputHandler.swift +++ b/Sources/axorc/Core/InputHandler.swift @@ -72,8 +72,16 @@ struct InputHandler { let sourceDescription = "File: \(filePath)" do { - let str = try String(contentsOfFile: filePath, encoding: .utf8) - .trimmingCharacters(in: .whitespacesAndNewlines) + let rawFileContent = try String(contentsOfFile: filePath, encoding: .utf8) // Read raw + if debug { + debugLogs.append("HFI_DEBUG: Raw file content for [\(filePath)]: '\(rawFileContent)' (length: \(rawFileContent.count))") + } + + let str = rawFileContent.trimmingCharacters(in: .whitespacesAndNewlines) + 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)") diff --git a/Sources/axorc/Models/AXORCModels.swift b/Sources/axorc/Models/AXORCModels.swift index abb5075..463ee77 100644 --- a/Sources/axorc/Models/AXORCModels.swift +++ b/Sources/axorc/Models/AXORCModels.swift @@ -1,7 +1,7 @@ // AXORCModels.swift - Response models and main types for AXORC CLI import ArgumentParser -import AXorcist +import AXorcistLib import Foundation // MARK: - Version and Configuration