Refactorings
This commit is contained in:
parent
67888c7d08
commit
3f335b489c
@ -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
|
||||
|
||||
@ -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: ¤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<String>(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<String>(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<String>(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<String>(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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: ¤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
|
||||
|
||||
15
Sources/AXorcist/Core/PathUtils.swift
Normal file
15
Sources/AXorcist/Core/PathUtils.swift
Normal 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]))
|
||||
}
|
||||
}
|
||||
@ -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)\\"}"
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
217
Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift
Normal file
217
Sources/AXorcist/Handlers/AXorcist+CollectAllHandler.swift
Normal 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
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// CommandExecutor.swift - Executes AXORC commands
|
||||
|
||||
import AXorcist
|
||||
import AXorcistLib
|
||||
import Foundation
|
||||
|
||||
struct CommandExecutor {
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user