Refactorings

This commit is contained in:
Peter Steinberger 2025-05-22 11:00:23 +02:00
parent 67888c7d08
commit 3f335b489c
13 changed files with 638 additions and 495 deletions

View File

@ -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

View File

@ -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<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
// logLocal("currentElementMatchesPathComponent: Element \(element.briefDescription(option: .minimal, isDebugLoggingEnabled: false, currentDebugLogs: &currentDebugLogs)) has '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
return true
}
}
return false
}
// Updated navigateToElement to prioritize children
@MainActor
internal func navigateToElement(
from startElement: Element,
@ -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: &currentDebugLogs
)
let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
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<String>(attributeName),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
if actualValueOnCurrent == expectedValue {
let briefDesc = currentElement.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
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: &currentDebugLogs
)
dLog("Child count: \(children?.count ?? 0)")
// Search children for the matching attribute and value
for child in children ?? [] {
if let actualValue = child.attribute(
Attribute<String>(attributeName),
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
) {
// Priority 1: Check children using Element.children()
if let childrenFromElementDotChildren = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)")
for child in childrenFromElementDotChildren {
let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
if let actualValue = child.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
// dLog("Child (from Element.children) \(childBriefDescForLog) has '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
if actualValue == expectedValue {
let childDesc = child.briefDescription(
option: .default,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
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: &currentDebugLogs) : "Element(debug_off)"
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Fallback is for element: \(currentElementDescForFallbackLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
if directChildrenError == .success, let cfArray = directChildrenValue, CFGetTypeID(cfArray) == CFArrayGetTypeID() {
if let directAxElements = cfArray as? [AXUIElement] {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback found \(directAxElements.count) raw children for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
for axChild in directAxElements {
let childElement = Element(axChild)
// let childBriefDescForLog = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) // Avoid for now inside loop if too verbose or risky
if let actualValue = childElement.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
if actualValue == expectedValue {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Matched child (from direct fallback) for '\(attributeName):\(expectedValue)' on \(currentElementDescForFallbackLog). Child: \(childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
newElementForNextStep = childElement
foundMatchForThisComponent = true
break
}
}
}
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: CFArray failed to cast to [AXUIElement] for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
} else if directChildrenError != .success {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: Error fetching for \(currentElementDescForFallbackLog): \(directChildrenError.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
} else {
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: No children or not an array for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
}
}
// Priority 2: If no child matched (even after fallback), check current element itself
if !foundMatchForThisComponent {
var tempLogsForMatchCheck = currentDebugLogs
let matchResult = AXorcist.currentElementMatchesPathComponent(
currentElement,
attributeName: attributeName,
expectedValue: expectedValue,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
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: &currentDebugLogs
)
dLog("Navigation successful. Final element: \(finalDesc)")
dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
return currentElement
}
}

View File

@ -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: &currentDebugLogs
)
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: &currentDebugLogs) : "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
}
}

View File

@ -26,13 +26,81 @@ public struct Element: Equatable, Hashable {
@MainActor
public func attribute<T>(_ attribute: Attribute<T>, 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: &currentDebugLogs
) 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

View File

@ -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]))
}
}

View File

@ -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: &currentDebugLogs) 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: &currentDebugLogs))")
guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) 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: &currentDebugLogs))")
}
dLog("[AXorcist.handlePerformAction] Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, depth: 0, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) 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: &currentDebugLogs)) 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: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to find element with locator: \(actualLocator) starting from \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))"
currentDebugLogs.append(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
targetElementForAction = foundElement
dLog("[AXorcist.handlePerformAction] Found element via locator: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
} else {
targetElementForAction = effectiveElement
dLog("[AXorcist.handlePerformAction] No locator provided. Using current effectiveElement as target: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
}
dLog("[AXorcist.handlePerformAction] Found element: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
dLog("[AXorcist.handlePerformAction] Element for action: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
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: &currentDebugLogs
) 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: &currentDebugLogs
) 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: &currentDebugLogs))")
}
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: &currentDebugLogs
) 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: &currentDebugLogs)), 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: &currentDebugLogs
)
// 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: &currentDebugLogs)) 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: &currentDebugLogs
) 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: &currentDebugLogs))"
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: &currentDebugLogs))")
} 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: &currentDebugLogs))")
}
dLog("[handleExtractText] Target element found: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)), attempting to extract text")
var attributes: [String: AnyCodable] = [:]
var extractedAnyText = false
if let 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: &currentDebugLogs) {
if CFGetTypeID(valueCF) == CFStringGetTypeID() {
let extractedValueText = valueCF as! String
if !extractedValueText.isEmpty {
attributes["extractedValue"] = AnyCodable(extractedValueText)
extractedAnyText = true
dLog("[handleExtractText] Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
} 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: &currentDebugLogs) {
if CFGetTypeID(selectedValueCF) == CFStringGetTypeID() {
let extractedSelectedText = selectedValueCF as! String
if !extractedSelectedText.isEmpty {
attributes["extractedSelectedText"] = AnyCodable(extractedSelectedText)
extractedAnyText = true
dLog("[handleExtractText] Extracted selected text from kAXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
} 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: &currentDebugLogs)
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)\\"}"
// }
// }
}

View File

@ -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: &currentDebugLogs
)

View File

@ -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
))
}
}

View File

@ -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)

View File

@ -1,6 +1,6 @@
// CommandExecutor.swift - Executes AXORC commands
import AXorcist
import AXorcistLib
import Foundation
struct CommandExecutor {

View File

@ -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)")

View File

@ -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