Major refactor

This commit is contained in:
Peter Steinberger 2025-05-27 03:07:10 +02:00
parent d45d5a672b
commit dacf59720b
43 changed files with 3052 additions and 3805 deletions

View File

@ -1,95 +0,0 @@
// AXorcist+CommandHandlers.swift - Command handler methods for AXorcist
import AppKit
import ApplicationServices
import Foundation
// MARK: - Command Handlers Extension
extension AXorcist {
// Placeholder for getting the focused element.
// It should accept debug logging parameters and update logs.
@MainActor
public func handleGetFocusedElement(
for appIdentifierOrNil: String? = nil,
requestedAttributes: [String]? = nil
) async -> HandlerResponse {
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey // Corrected: Use AXMiscConstants.focusedApplicationKey
axDebugLog("[AXorcist.handleGetFocusedElement] Handling for app: \(appIdentifier)",
file: #file,
function: #function,
line: #line
)
guard let appElement = applicationElement(for: appIdentifier) else {
let errorMsgText = "Application not found: \(appIdentifier)"
axDebugLog("[AXorcist.handleGetFocusedElement] \(errorMsgText)",
file: #file,
function: #function,
line: #line
)
return HandlerResponse(data: nil, error: errorMsgText)
}
axDebugLog("[AXorcist.handleGetFocusedElement] Successfully obtained application element for \(appIdentifier)",
file: #file,
function: #function,
line: #line
)
var cfValue: CFTypeRef?
let copyAttributeStatus = AXUIElementCopyAttributeValue(
appElement.underlyingElement,
AXAttributeNames.kAXFocusedUIElementAttribute as CFString,
&cfValue
)
guard copyAttributeStatus == .success, let rawAXElement = cfValue else {
axDebugLog(
"[AXorcist.handleGetFocusedElement] Failed to copy focused element attribute or it was nil. " +
"Status: \(axErrorToString(copyAttributeStatus)). Application: \(appIdentifier)",
file: #file,
function: #function,
line: #line
)
return HandlerResponse(
data: nil,
error: "Could not get the focused UI element for \(appIdentifier). Ensure a window of the application is focused. AXError: \(axErrorToString(copyAttributeStatus))"
)
}
guard CFGetTypeID(rawAXElement) == AXUIElementGetTypeID() else {
axDebugLog(
"[AXorcist.handleGetFocusedElement] Focused element attribute was not an AXUIElement. Application: \(appIdentifier)",
file: #file,
function: #function,
line: #line
)
return HandlerResponse(
data: nil,
error: "Focused element was not a valid UI element for \(appIdentifier)."
)
}
let focusedElement = Element(rawAXElement as! AXUIElement)
axDebugLog(
"[AXorcist.handleGetFocusedElement] Successfully obtained focused element: " +
"\(focusedElement.briefDescription()) for application \(appIdentifier)",
file: #file,
function: #function,
line: #line
)
let (fetchedAttributes, _) = await getElementAttributes(
element: focusedElement,
attributes: requestedAttributes ?? [],
outputFormat: .smart
)
let elementPathArray = focusedElement.generatePathArray(upTo: appElement)
let axElement = AXElement(attributes: fetchedAttributes, path: elementPathArray)
return HandlerResponse(data: AnyCodable(axElement), error: nil)
}
}

View File

@ -1,214 +0,0 @@
import AppKit
import ApplicationServices
import Foundation
// GlobalAXLogger is expected to be available
// Global constant for backwards compatibility - removed, now using AXMiscConstants.defaultMaxDepthSearch
// Placeholder for the actual accessibility logic.
// For now, this module is very thin and AXorcist.swift is the main public API.
// Other files like Element.swift, Models.swift, Search.swift, etc. are in Core/ Utils/ etc.
public class AXorcist {
// let focusedAppKeyValue = "focused" // Replaced by AXMiscConstants.focusedApplicationKey
// Removed recursiveCallDebugLogs, as GlobalAXLogger handles accumulation
// MARK: - Focus Tracking State (used by AXorcist+FocusTracking.swift)
internal var focusTrackingObserver: AXObserver?
internal var focusTrackingPID: pid_t = 0
internal var focusTrackingCallback: AXFocusChangeCallback?
internal var focusedUIElementToken: AXObserverCenter.SubscriptionToken?
internal var focusedWindowToken: AXObserverCenter.SubscriptionToken?
internal var systemWideFocusToken: AXObserverCenter.SubscriptionToken? // For system-wide tracking
// Default values for collection and search if not provided by the command
// These are now primarily defined in AXMiscConstants and can be referenced from there.
// Public static let defaultMaxDepthCollectAll = AXMiscConstants.defaultMaxDepthCollectAll
// Public static let defaultMaxDepthSearch = AXMiscConstants.defaultMaxDepthSearch
// Public static let defaultMaxDepthPathResolution = AXMiscConstants.defaultMaxDepthPathResolution
// Public static let defaultMaxDepthDescribe = AXMiscConstants.defaultMaxDepthDescribe
// Public static let defaultTimeoutPerElementCollectAll = AXMiscConstants.defaultTimeoutPerElementCollectAll
// Default attributes to fetch if none are specified by the command.
// This can also be moved to AXMiscConstants if it's a shared default, or kept here if specific to AXorcist class logic.
public static let defaultAttributesToFetch: [String] = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXTitleAttribute,
AXAttributeNames.kAXSubroleAttribute,
AXAttributeNames.kAXIdentifierAttribute,
AXAttributeNames.kAXDescriptionAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXSelectedTextAttribute,
AXAttributeNames.kAXEnabledAttribute,
AXAttributeNames.kAXFocusedAttribute
]
public init() {
// Logging is now managed by GlobalAXLogger and per-call startCollecting/stopCollecting logic
}
// Removed static func formatDebugLogMessage - GlobalAXLogger handles formatting
// Handler methods are implemented in extension files:
// - handlePerformAction: AXorcist+ActionHandlers.swift
// - handleExtractText: AXorcist+ActionHandlers.swift
// - handleCollectAll: AXorcist+ActionHandlers.swift
// - handleBatchCommands: AXorcist+BatchHandler.swift
// Handler methods are implemented in extension files:
// - handleExtractText: AXorcist+ActionHandlers.swift
// - handleBatchCommands: AXorcist+BatchHandler.swift
// - handleCollectAll: AXorcist+CollectAllHandler.swift
// MARK: - Path Navigation
// MARK: - Search Operations
@MainActor
public func search(
element: Element,
locator: Locator,
requireAction: String? = nil,
depth: Int = 0,
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) async -> Element? {
axDebugLog("AXorcist.search: Starting search from element \(element.briefDescription()). Locator: \(locator)")
// findElementViaCriteriaAndJSONPathHint is async
let foundElement = await findElementViaCriteriaAndJSONPathHint(
application: element,
locator: locator,
maxDepth: maxDepth
)
if foundElement != nil {
axDebugLog("AXorcist.search: findElementViaCriteriaAndJSONPathHint found an element.")
} else {
axDebugLog("AXorcist.search: findElementViaCriteriaAndJSONPathHint did NOT find an element.")
}
return foundElement
}
// MARK: - Observe Command Handler
@MainActor
public func handleObserve(
for appIdentifierOrNil: String?,
notifications: [String],
includeElementDetails: [String],
watchChildren: Bool,
commandId: String,
debugCLI: Bool
) async -> Bool {
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
axInfoLog("[AXorcist.handleObserve][CmdID: \(commandId)] Starting observe for app: \(appIdentifier), notifications: \(notifications.joined(separator: ", ")), details: \(includeElementDetails.joined(separator: ", "))")
// applicationElement is sync
guard let appElement = applicationElement(for: appIdentifier) else {
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Application not found: \(appIdentifier)")
return false
}
var subscriptionTokens: [AXObserverCenter.SubscriptionToken] = []
let observerCallback: AXNotificationSubscriptionHandler = {
obsPid, notificationNameString, rawObservedElement, nsUserInfo in
let observedElement = Element(rawObservedElement)
// generatePathArray is sync, pid() is sync
let elementPath = observedElement.generatePathArray(upTo: appElement.pid() == obsPid ? appElement : nil)
// Since getElementAttributes is async but we're in a sync context,
// we need to use Task to call it
Task { @MainActor in
let (attributes, _) = await getElementAttributes(
element: observedElement,
attributes: includeElementDetails,
outputFormat: .smart
)
var sanitizedElement: [String: Any] = [:]
if !attributes.isEmpty {
var sanitizedAttrs: [String: Any] = [:]
for (k, v) in attributes {
sanitizedAttrs[k] = sanitizeValue(v.value)
}
sanitizedElement["attributes"] = sanitizedAttrs
}
if !elementPath.isEmpty {
sanitizedElement["path"] = elementPath
}
let payloadRaw: [String: Any] = [
"timestamp": Date().timeIntervalSince1970,
"commandId": commandId,
"notification": notificationNameString.rawValue,
"pid": obsPid,
"application": appIdentifier,
"element": sanitizedElement.mapValues { $0 }
]
let safePayload = makeJSONCompatible(payloadRaw) as! [String: Any]
if let data = try? JSONSerialization.data(withJSONObject: safePayload, options: []),
let jsonStr = String(data: data, encoding: .utf8) {
fputs("\(jsonStr)\n", stdout)
fflush(stdout)
} else {
fputs("{\"error\": \"Unencodable payload\"}\n", stderr)
fflush(stderr)
}
}
}
var allSubscriptionsSuccessful = true
for notificationNameString in notifications {
guard let axNotificationName = AXNotification(rawValue: notificationNameString) else {
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Invalid notification name string: \(notificationNameString). Skipping.")
continue
}
// pid() is sync
guard let targetPid = appElement.pid() else {
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Could not get PID for appElement: \(appIdentifier)")
allSubscriptionsSuccessful = false
break
}
let result = AXObserverCenter.shared.subscribe(
pid: targetPid,
element: appElement,
notification: axNotificationName,
handler: observerCallback
)
switch result {
case .success(let token):
subscriptionTokens.append(token)
axDebugLog("[AXorcist.handleObserve][CmdID: \(commandId)] Subscribed to \(notificationNameString) for \(appIdentifier) (PID: \(targetPid))")
case .failure(let error):
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Error subscribing to \(notificationNameString) for \(appIdentifier): \(error.description)")
allSubscriptionsSuccessful = false
break
}
if !allSubscriptionsSuccessful { break }
}
if !allSubscriptionsSuccessful || subscriptionTokens.isEmpty {
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Failed to subscribe to one or more notifications for \(appIdentifier). Cleaning up...")
for token in subscriptionTokens {
do {
try AXObserverCenter.shared.unsubscribe(token: token)
axDebugLog("[AXorcist.handleObserve][CmdID: \(commandId)] Unsubscribed token \(token.id) during cleanup.")
} catch {
axErrorLog("[AXorcist.handleObserve][CmdID: \(commandId)] Error unsubscribing token \(token.id) during cleanup: \(error.localizedDescription)")
}
}
return false
}
axInfoLog("[AXorcist.handleObserve][CmdID: \(commandId)] Successfully subscribed to \(subscriptionTokens.count) notifications for \(appIdentifier). Streaming output to stdout.")
return true
}
}
// NOTE: The global function `findElementViaPathAndCriteria`

View File

@ -0,0 +1,145 @@
import Foundation
import ApplicationServices
import AppKit // For NSRunningApplication & NSValue
@MainActor
extension AXorcist {
// MARK: - Perform Action Handler
public func handlePerformAction(command: PerformActionCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandlePerformAction: App '\(String(describing: command.appIdentifier))', Locator: \(command.locator), Action: \(command.action), Value: \(String(describing: command.value))"))
let (foundElement, error) = findTargetElement(
for: command.appIdentifier ?? "focused",
locator: command.locator,
maxDepthForSearch: command.maxDepthForSearch
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandlePerformAction: Element not found for app '\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandlePerformAction: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
// Check if action is supported before attempting
if !element.isActionSupported(command.action) {
let errorMessage = "AXorcist/HandlePerformAction: Action '\(command.action)' is NOT supported by element \(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: errorMessage))
// Get available actions for better error reporting
let availableActions = element.supportedActions() ?? []
return .errorResponse(message: "\(errorMessage) Available actions: [\(availableActions.joined(separator: ", "))]", code: .actionNotSupported)
}
do {
// Note: The performAction method doesn't take a value parameter
// If the action requires a value, it should be set separately
if let actionValue = command.value?.value {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "AXorcist/HandlePerformAction: Action value provided but not used: \(actionValue)"))
}
try element.performAction(command.action)
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandlePerformAction: Successfully performed action '\(command.action)' on \(element.briefDescription(option: ValueFormatOption.smart))."))
return .successResponse(payload: AnyCodable(["message": "Action '\(command.action)' performed successfully."]))
} catch {
let errorMessage = "AXorcist/HandlePerformAction: Failed to perform action '\(command.action)' on \(element.briefDescription(option: ValueFormatOption.smart)). Error: \(error)"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .actionFailed)
}
}
// MARK: - Set Focused Value Handler
public func handleSetFocusedValue(command: SetFocusedValueCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleSetFocusedValue: App '\(String(describing: command.appIdentifier))', Locator: \(command.locator), Value: '\(command.value)'"))
let (foundElement, error) = findTargetElement(
for: command.appIdentifier ?? "focused",
locator: command.locator,
maxDepthForSearch: command.maxDepthForSearch
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandleSetFocusedValue: Element not found for app '\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleSetFocusedValue: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
// Make sure the element is focusable, then focus it, then set its value.
// This is a common pattern for text fields.
// 1. Check if focusable (kAXFocusedAttribute should be settable)
var isFocusable = false
// Check if the element can have the focused attribute set
if element.isAttributeSettable(named: AXAttributeNames.kAXFocusedAttribute) {
isFocusable = true
}
if !isFocusable {
// If not directly focusable by kAXFocusedAttribute, check if it can perform kAXPressAction, which might make it focusable.
if element.isActionSupported(AXActionNames.kAXPressAction) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleSetFocusedValue: Element not directly focusable by kAXFocusedAttribute, but supports kAXPressAction. Attempting press."))
do {
try element.performAction(AXActionNames.kAXPressAction)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleSetFocusedValue: Successfully pressed element to potentially gain focus."))
} catch {
let pressError = "AXorcist/HandleSetFocusedValue: Element \(element.briefDescription(option: ValueFormatOption.smart)) could not be pressed to potentially gain focus."
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: pressError))
// Continue to try setting value, but log this warning.
}
} else {
let focusError = "AXorcist/HandleSetFocusedValue: Element \(element.briefDescription(option: ValueFormatOption.smart)) is not focusable (kAXFocusedAttribute not settable and kAXPressAction not supported). Cannot reliably set focused value."
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: focusError))
// Proceed to set value anyway, but this is a warning.
}
}
// 2. Attempt to set focus (best effort)
if isFocusable {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleSetFocusedValue: Attempting to set kAXFocusedAttribute to true for \(element.briefDescription(option: ValueFormatOption.smart))"))
if !element.setValue(true, forAttribute: AXAttributeNames.kAXFocusedAttribute) {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "AXorcist/HandleSetFocusedValue: Failed to set kAXFocusedAttribute for \(element.briefDescription(option: ValueFormatOption.smart)), but proceeding to set value."))
} else {
// Short delay to allow UI to catch up after focusing, if necessary. Consider if this is needed based on app behavior.
// Thread.sleep(forTimeInterval: 0.05)
}
}
// 3. Set the value (kAXValueAttribute)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleSetFocusedValue: Attempting to set kAXValueAttribute to '\(command.value)' for \(element.briefDescription(option: ValueFormatOption.smart))"))
if element.setValue(command.value, forAttribute: AXAttributeNames.kAXValueAttribute) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleSetFocusedValue: Successfully set value for \(element.briefDescription(option: ValueFormatOption.smart))."))
return .successResponse(payload: AnyCodable(["message": "Value '\(command.value)' set successfully on focused element."]))
} else {
let setError = "AXorcist/HandleSetFocusedValue: Failed to set kAXValueAttribute for \(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: setError))
return .errorResponse(message: setError, code: .actionFailed)
}
}
// MARK: - Extract Text Handler
public func handleExtractText(command: ExtractTextCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleExtractText: App '\(String(describing: command.appIdentifier))', Locator: \(command.locator), IncludeChildren: \(String(describing: command.includeChildren)), MaxDepth: \(String(describing: command.maxDepth))"))
let (foundElement, error) = findTargetElement(
for: command.appIdentifier ?? "focused",
locator: command.locator,
maxDepthForSearch: command.maxDepthForSearch
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandleExtractText: Element not found for app '\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleExtractText: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
if let textContent = getElementTextualContent(element: element, includeChildren: command.includeChildren ?? true, maxDepth: command.maxDepth ?? 5) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleExtractText: Extracted text: '\(textContent)'"))
return .successResponse(payload: AnyCodable(TextPayload(text: textContent)))
} else {
let message = "AXorcist/HandleExtractText: No text content found for element \(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: message))
return .successResponse(payload: AnyCodable(TextPayload(text: ""))) // Success, but no text
}
}
}

View File

@ -0,0 +1,63 @@
import Foundation
@MainActor
extension AXorcist {
public func handleBatchCommands(command: BatchCommandEnvelope) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleBatch: Received \(command.commands.count) sub-commands."))
var results: [AXResponse] = []
var overallSuccess = true
var errorMessages: [String] = []
for (index, subCommandEnvelope) in command.commands.enumerated() {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleBatch: Processing sub-command \(index + 1)/\(command.commands.count): ID '\(subCommandEnvelope.commandID)', Type: \(subCommandEnvelope.command.type)"))
let response = processSingleBatchCommand(subCommandEnvelope.command)
results.append(response)
if response.status != "success" {
overallSuccess = false
let errorDetail = response.error?.message ?? "Unknown error in sub-command \(subCommandEnvelope.commandID)"
errorMessages.append("Sub-command \(subCommandEnvelope.commandID) ('\(subCommandEnvelope.command.type)') failed: \(errorDetail)")
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "AXorcist/HandleBatch: Sub-command \(subCommandEnvelope.commandID) failed: \(errorDetail)"))
}
}
if overallSuccess {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleBatch: All \(command.commands.count) sub-commands succeeded."))
let successfulPayloads = results.map { $0.payload }
return .successResponse(payload: AnyCodable(BatchResponsePayload(results: successfulPayloads, errors: nil)))
} else {
let combinedErrorMessage = "AXorcist/HandleBatch: One or more sub-commands failed. Errors: \(errorMessages.joined(separator: "; "))"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: combinedErrorMessage))
return .errorResponse(message: combinedErrorMessage, code: .batchOperationFailed)
}
}
private func processSingleBatchCommand(_ command: AXSubCommand) -> AXResponse {
switch command {
case .query(let queryCommand):
return handleQuery(command: queryCommand, maxDepth: queryCommand.maxDepthForSearch)
case .performAction(let actionCommand):
return handlePerformAction(command: actionCommand)
case .getAttributes(let getAttributesCommand):
return handleGetAttributes(command: getAttributesCommand)
case .describeElement(let describeCommand):
return handleDescribeElement(command: describeCommand)
case .extractText(let extractTextCommand):
return handleExtractText(command: extractTextCommand)
case .setFocusedValue(let setFocusedValueCommand):
return handleSetFocusedValue(command: setFocusedValueCommand)
case .getElementAtPoint(let getElementAtPointCommand):
return handleGetElementAtPoint(command: getElementAtPointCommand)
case .getFocusedElement(let getFocusedElementCommand):
return handleGetFocusedElement(command: getFocusedElementCommand)
case .observe(let observeCommand):
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/BatchProc: Processing Observe command."))
return handleObserve(command: observeCommand)
case .collectAll(let collectAllCommand):
return handleCollectAll(command: collectAllCommand)
case .batch:
return .errorResponse(message: "Nested batch commands are not supported within a single batch operation.", code: .invalidCommand)
}
}
}

View File

@ -0,0 +1,30 @@
import Foundation
import ApplicationServices
@MainActor
extension AXorcist {
public func handleGetFocusedElement(command: GetFocusedElementCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleGetFocused: App '\(String(describing: command.appIdentifier))', Attributes: \(command.attributesToReturn?.joined(separator: ", ") ?? "default")"))
guard let appElement = getApplicationElement(for: command.appIdentifier ?? "focused") else {
let errorMessage = "AXorcist/HandleGetFocused: Could not get application element for '\(String(describing: command.appIdentifier))'."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetFocused: Got app element: \(appElement.briefDescription(option: ValueFormatOption.smart))"))
guard let focusedElement = appElement.focusedUIElement() else {
let errorMessage = "AXorcist/HandleGetFocused: No focused element found for application '\(String(describing: command.appIdentifier))' (\(appElement.briefDescription(option: ValueFormatOption.smart))])."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: errorMessage))
// This is not necessarily an error, could be a valid state.
// Return success with an empty payload or specific indication.
return .successResponse(payload: AnyCodable(NoFocusPayload(message: "No focused element found.")))
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetFocused: Focused element: \(focusedElement.briefDescription(option: ValueFormatOption.smart))"))
let attributesToFetch = command.attributesToReturn ?? AXMiscConstants.defaultAttributesToFetch
let elementData = buildQueryResponse(element: focusedElement, attributesToFetch: attributesToFetch, includeChildrenBrief: command.includeChildrenBrief ?? false)
return .successResponse(payload: AnyCodable(elementData))
}
}

View File

@ -0,0 +1,53 @@
import Foundation
import ApplicationServices // For CGPoint
@MainActor
extension AXorcist {
public func handleGetElementAtPoint(command: GetElementAtPointCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleGetElementAtPoint: App '\(command.appIdentifier ?? "focused")', Point: ([\(command.point.x), \(command.point.y)]), PID: \(command.pid ?? 0)"))
// Get the application element first to ensure the coordinate system context.
// While elementAtPoint is system-wide, it's good practice to ensure app context if specified.
guard let appElement = getApplicationElement(for: command.appIdentifier ?? "focused") else {
let errorMessage = "AXorcist/HandleGetElementAtPoint: Could not get application element for '\(command.appIdentifier ?? "focused")'. This is needed for context, even if elementAtPoint is system-wide."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound) // Or perhaps a different error code if app context is just preferred
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetElementAtPoint: Context app element: \(appElement.briefDescription(option: ValueFormatOption.smart))"))
let pid: pid_t = command.pid.map { pid_t($0) } ?? appElement.pid() ?? 0
guard let elementAtPoint = Element.elementAtPoint(command.point, pid: pid) else {
let errorMessage = "AXorcist/HandleGetElementAtPoint: No UI element found at point ([\(command.point.x), \(command.point.y)]) for app context '\(command.appIdentifier ?? "focused")'."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: errorMessage))
// This is not necessarily an error, could be a valid state (e.g., clicked on desktop).
// Return success with an empty payload or specific indication.
struct NoElementAtPointPayload: Codable {
let message: String
let element: AXElementData?
init(message: String, element: AXElementData? = nil) {
self.message = message
self.element = element
}
}
return .successResponse(payload: AnyCodable(NoElementAtPointPayload(message: "No UI element found at the specified point.")))
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetElementAtPoint: Element at point: \(elementAtPoint.briefDescription(option: ValueFormatOption.smart))"))
// Build a response with the element information
let briefDescription = elementAtPoint.briefDescription(option: ValueFormatOption.smart)
let role = elementAtPoint.role()
let elementData = AXElementData(
briefDescription: briefDescription,
role: role,
attributes: [:], // Could fetch attributes if needed
allPossibleAttributes: elementAtPoint.attributeNames(),
textualContent: nil,
childrenBriefDescriptions: nil,
fullAXDescription: elementAtPoint.briefDescription(option: ValueFormatOption.stringified),
path: elementAtPoint.path()?.components
)
return .successResponse(payload: AnyCodable(elementData))
}
}

View File

@ -0,0 +1,52 @@
import Foundation
import ApplicationServices
@MainActor
extension AXorcist {
public func handleObserve(command: ObserveCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleObserve: App \(command.appIdentifier ?? "focused"), Notifications: \(command.notificationName.rawValue), Details: \(command.includeElementDetails?.joined(separator: ", ") ?? "none")"))
let appIdentifier = command.appIdentifier ?? "focused"
// Use Criterion for pid matching
let criteria = [Criterion(attribute: "pid", value: "self", matchType: .exact)]
let locator = Locator(criteria: criteria)
let (targetElement, error) = findTargetElement(
for: appIdentifier,
locator: locator,
maxDepthForSearch: command.maxDepthForSearch
)
guard let elementToObserve = targetElement else {
let errorMessage = error ?? "AXorcist/HandleObserve: Element to observe not found for app '\(appIdentifier)' with locator \(String(describing: locator))."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleObserve: Element to observe: \(elementToObserve.briefDescription(option: ValueFormatOption.smart))"))
let callback: AXObserverManager.AXNotificationCallback = { observer, axUIElement, notification, userInfo in
let element = Element(axUIElement)
let userInfoDesc = userInfo != nil ? String(describing: userInfo!) : "nil"
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXObserver CALLBACK: Element: \(element.briefDescription(option: ValueFormatOption.smart)), Notification: \(notification as String), UserInfo: \(userInfoDesc)"))
// Here, you would typically send this event data back to the client that initiated the observation.
// This might involve a registered callback URL, a WebSocket, or another IPC mechanism.
// For now, we just log it.
}
do {
try AXObserverManager.shared.addObserver(for: elementToObserve, notification: command.notificationName, callback: callback)
let successMessage = "AXorcist/HandleObserve: Successfully started observing '\(command.notificationName)' on \(elementToObserve.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: successMessage))
return .successResponse(payload: AnyCodable(["message": successMessage]))
} catch let obError as AXObserverManager.ObserverError {
let errorMessage = "AXorcist/HandleObserve: Failed to add observer. Error: \(obError.localizedDescription) (Code: \(obError)). Pid for element: \(elementToObserve.pid()?.description ?? "N/A") Notification: \(command.notificationName)"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .observationFailed)
} catch {
let errorMessage = "AXorcist/HandleObserve: Failed to add observer with unknown error: \(error.localizedDescription) for element \(elementToObserve.briefDescription(option: ValueFormatOption.smart)) Notification: \(command.notificationName)"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .observationFailed)
}
}
}

View File

@ -0,0 +1,181 @@
import Foundation
import ApplicationServices
import AppKit // For NSRunningApplication
// Note: Assumes Element, Attribute, AXValueWrapper, etc. are defined and accessible.
// Assumes GlobalAXLogger is available.
@MainActor
extension AXorcist {
// MARK: - Query Handler
public func handleQuery(command: QueryCommand, maxDepth externalMaxDepth: Int?) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleQuery: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator)"))
let appIdentifier = command.appIdentifier ?? "focused"
let resolvedMaxDepth = externalMaxDepth ?? 10
// DEBUG LOG FOR MAX DEPTH
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleQuery: externalMaxDepth = \(String(describing: externalMaxDepth)), resolved maxDepth = \(resolvedMaxDepth)"))
let (foundElement, findError) = findTargetElement(
for: appIdentifier,
locator: command.locator,
maxDepthForSearch: resolvedMaxDepth
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleQuery: Element not found for app '\(appIdentifier)' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleQuery: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
// Fetch attributes specified in command.attributesToReturn, or default if nil/empty
let attributesToFetch = command.attributesToReturn ?? AXMiscConstants.defaultAttributesToFetch
let elementData = buildQueryResponse(element: element, attributesToFetch: attributesToFetch, includeChildrenBrief: command.includeChildrenBrief ?? false)
return .successResponse(payload: AnyCodable(elementData))
}
// MARK: - Get Attributes Handler
public func handleGetAttributes(command: GetAttributesCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleGetAttrs: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator), Attributes: \(command.attributes.joined(separator: ", "))"))
let (foundElement, findError) = findTargetElement(
for: command.appIdentifier ?? "focused",
locator: command.locator,
maxDepthForSearch: command.maxDepthForSearch
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleGetAttrs: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetAttrs: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
var attributesDict: [String: AXValueWrapper] = [:]
for attrName in command.attributes {
if let value: Any = element.attribute(Attribute<Any>(attrName)) {
attributesDict[attrName] = AXValueWrapper(value: value)
} else {
attributesDict[attrName] = AXValueWrapper(value: nil) // Explicitly store nil for missing attributes
}
}
let briefDesc = element.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetAttrs: Attributes for '\(briefDesc)': \(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"))
// Log fetched attributes for debugging purposes
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/GetAttributes: Fetched attributes for \(briefDesc): \(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"))
// Construct a simple payload containing just the attributes dictionary.
// For a more structured response like AXElementData, we'd use buildQueryResponse or similar.
struct AttributesPayload: Codable {
let attributes: [String: AXValueWrapper]
let elementDescription: String
}
let payload = AttributesPayload(attributes: attributesDict, elementDescription: briefDesc)
return .successResponse(payload: AnyCodable(payload))
}
// MARK: - Describe Element Handler
public func handleDescribeElement(command: DescribeElementCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleDescribe: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator), Depth: \(command.depth), IncludeIgnored: \(command.includeIgnored)"))
let (foundElement, findError) = findTargetElement(
for: command.appIdentifier ?? "focused",
locator: command.locator,
maxDepthForSearch: command.maxSearchDepth
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleDescribe: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleDescribe: Found element: \(element.briefDescription(option: ValueFormatOption.smart)). Describing tree..."))
let descriptionTree = describeElementTree(element: element, depth: command.depth, includeIgnored: command.includeIgnored, currentDepth: 0)
return .successResponse(payload: AnyCodable(descriptionTree))
}
// MARK: - Helper Methods for Querying
internal func buildQueryResponse(element: Element, attributesToFetch: [String], includeChildrenBrief: Bool) -> AXElementData {
let fetchedAttributes = fetchInstanceElementAttributes(element: element, attributeNames: attributesToFetch)
// Get all possible attribute names for this element
let allAXAttributes = element.attributeNames()
let textualContent = extractTextFromElement(element, maxDepth: 3) // MaxDepth set to 3 for brief text
let childrenBriefs = includeChildrenBrief ? (element.children()?.map { $0.briefDescription(option: ValueFormatOption.smart) } ?? []) : nil
let fullDesc = element.briefDescription(option: .stringified) // Using .stringified for a detailed description
let pathArray = element.path()?.components // Assuming path() returns an optional Path struct with a components array
let briefDescription = element.briefDescription(option: ValueFormatOption.smart)
let role = element.role()
// let fullDescription = element.briefDescription(option: .stringified) // This is synchronous - Commented out as unused
return AXElementData(
briefDescription: briefDescription,
role: role,
attributes: fetchedAttributes,
allPossibleAttributes: allAXAttributes,
textualContent: textualContent,
childrenBriefDescriptions: childrenBriefs,
fullAXDescription: fullDesc,
path: pathArray
)
}
private func describeElementTree(element: Element, depth: Int, includeIgnored: Bool, currentDepth: Int) -> AXElementDescription {
if !includeIgnored && element.isIgnored() {
// Return a minimal description for an ignored element if not including them
return AXElementDescription(
briefDescription: element.briefDescription(option: ValueFormatOption.smart) + " (Ignored)",
role: element.role(),
attributes: [:], // No attributes for ignored elements unless explicitly asked
children: nil
)
}
let attributes = fetchInstanceElementAttributes(element: element, attributeNames: AXMiscConstants.defaultAttributesToFetch)
var childrenDescriptions: [AXElementDescription]? = nil
if currentDepth < depth {
if let children = element.children() {
childrenDescriptions = []
for child in children {
if !includeIgnored && child.isIgnored() {
continue // Skip ignored children if not including them
}
childrenDescriptions?.append(describeElementTree(element: child, depth: depth, includeIgnored: includeIgnored, currentDepth: currentDepth + 1))
}
if childrenDescriptions?.isEmpty ?? true { childrenDescriptions = nil }
}
}
return AXElementDescription(
briefDescription: element.briefDescription(option: ValueFormatOption.smart),
role: element.role(),
attributes: attributes,
children: childrenDescriptions
)
}
private func fetchInstanceElementAttributes(element: Element, attributeNames: [String]) -> [String: AXValueWrapper] {
var attributesDict: [String: AXValueWrapper] = [:]
for name in attributeNames {
if let value: Any = element.attribute(Attribute<Any>(name)) {
attributesDict[name] = AXValueWrapper(value: value)
} else {
// For attributes explicitly requested but not found, we might represent them as nil
// or simply omit them. Current AXValueWrapper handles nils.
attributesDict[name] = AXValueWrapper(value: nil)
}
}
return attributesDict
}
}

View File

@ -0,0 +1,71 @@
import Foundation
import ApplicationServices
import AppKit // For NSRunningApplication
// Main class for AXorcist operations
@MainActor
public class AXorcist {
@MainActor public init() {}
public static let shared = AXorcist()
private let logger = GlobalAXLogger.shared // Use the shared logger
// Central command processing function
public func runCommand(_ commandEnvelope: AXCommandEnvelope) -> AXResponse { // Removed async
logger.log(AXLogEntry(level: .info, message: "AXorcist/RunCommand: ID '\(commandEnvelope.commandID)', Type: \(commandEnvelope.command.type)")) // Removed await
let response: AXResponse
switch commandEnvelope.command {
case .query(let queryCommand):
response = handleQuery(command: queryCommand, maxDepth: queryCommand.maxDepthForSearch)
case .performAction(let actionCommand):
response = handlePerformAction(command: actionCommand) // Removed await
case .getAttributes(let getAttributesCommand):
response = handleGetAttributes(command: getAttributesCommand) // Removed await
case .describeElement(let describeCommand):
response = handleDescribeElement(command: describeCommand) // Removed await
case .extractText(let extractTextCommand):
response = handleExtractText(command: extractTextCommand) // Removed await
case .batch(let batchCommandEnvelope):
// The batch command itself is an envelope, pass it directly to handleBatchCommands.
response = handleBatchCommands(command: batchCommandEnvelope) // Removed await
case .setFocusedValue(let setFocusedValueCommand):
response = handleSetFocusedValue(command: setFocusedValueCommand) // Removed await
case .getElementAtPoint(let getElementAtPointCommand):
response = handleGetElementAtPoint(command: getElementAtPointCommand) // Removed await
case .getFocusedElement(let getFocusedElementCommand):
response = handleGetFocusedElement(command: getFocusedElementCommand) // Removed await
case .observe(let observeCommand):
response = handleObserve(command: observeCommand) // Removed await
case .collectAll(let collectAllCommand):
response = handleCollectAll(command: collectAllCommand)
// Add other command types here
// default:
// let errormsg = "AXorcist/RunCommand: Unknown command type: \(commandEnvelope.command.type)"
// logger.log(AXLogEntry(level: .error, message: errormsg))
// response = .errorResponse(message: errormsg, code: .unknownCommand)
}
logger.log(AXLogEntry(level: .info, message: "AXorcist/RunCommand ID '\(commandEnvelope.commandID)' completed. Status: \(response.status)")) // Removed await
return response
}
// MARK: - CollectAll Handler (New)
internal func handleCollectAll(command: CollectAllCommand) -> AXResponse {
// Placeholder implementation - replace with actual logic
logger.log(AXLogEntry(level: .info, message: "AXorcist/HandleCollectAll: Command received for app '\(command.appIdentifier ?? "nil")'. Not yet fully implemented."))
// TODO: Implement actual collect all logic using command.appIdentifier, command.attributesToReturn, command.maxDepth, command.filterCriteria, command.valueFormatOption
return .errorResponse(message: "CollectAll command not yet fully implemented.", code: .unknownCommand)
}
// MARK: - Logger Methods
public func getLogs() -> [String] {
return GlobalAXLogger.shared.getLogsAsStrings()
}
public func clearLogs() {
GlobalAXLogger.shared.clearEntries()
logger.log(AXLogEntry(level: .info, message: "AXorcist log history cleared."))
}
}

View File

@ -286,7 +286,7 @@ public enum AXRoleNames {
}
// MARK: - Accessibility Notification Names (Moved from AXNotificationConstants.swift)
public enum AXNotification: String {
public enum AXNotification: String, Sendable {
// System-Wide Notifications
case mainWindowChanged = "AXMainWindowChanged" // kAXMainWindowChangedNotification
case focusedWindowChanged = "AXFocusedWindowChanged" // kAXFocusedWindowChangedNotification
@ -341,6 +341,21 @@ public enum AXNotification: String {
public enum AXMiscConstants {
public static let axBinaryVersion = "0.8.0" // AXorcist version for this constants file
// Default attributes to fetch when none are specified
public static let defaultAttributesToFetch: [String] = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXSubroleAttribute,
AXAttributeNames.kAXTitleAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXIdentifierAttribute,
AXAttributeNames.kAXDescriptionAttribute,
AXAttributeNames.kAXEnabledAttribute,
AXAttributeNames.kAXFocusedAttribute,
AXAttributeNames.kAXPositionAttribute,
AXAttributeNames.kAXSizeAttribute,
AXAttributeNames.kAXChildrenAttribute // To get an idea of hierarchy
]
// Default values for collection and search
public static let defaultMaxDepthCollectAll = 5
public static let defaultMaxDepthSearch = 10

View File

@ -1,161 +0,0 @@
import Foundation
import CoreGraphics // Import for CGPoint, CGSize, CGRect
import Accessibility // Import for AXTextMarker, AXTextMarkerRange
import ApplicationServices // For AXUIElement
// It's likely AXorcist module, where this file lives, already imports Accessibility or AppKit,
// which would make AXTextMarker and AXTextMarkerRange available.
// If not, a more dynamic type check might be needed, or this file needs to import them.
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
// Note: @unchecked Sendable is used because 'Any' cannot guarantee thread safety
public struct AnyCodable: Codable, @unchecked Sendable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let decodedValue = try AnyCodable.decodeValue(from: container) {
self.value = decodedValue
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "AnyCodable value cannot be decoded"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if try encodeValue(value, to: &container) {
return
}
let context = EncodingError.Context(
codingPath: container.codingPath,
debugDescription: "AnyCodable value cannot be encoded"
)
throw EncodingError.invalidValue(value, context)
}
// MARK: - Private Helper Methods
private static func decodeValue(from container: SingleValueDecodingContainer) throws -> Any? {
if container.decodeNil() {
return ()
}
// Try primitive types
if let primitiveValue = try decodePrimitiveValue(from: container) {
return primitiveValue
}
// Try collection types
if let collectionValue = try decodeCollectionValue(from: container) {
return collectionValue
}
return nil
}
private static func decodePrimitiveValue(from container: SingleValueDecodingContainer) throws -> Any? {
if let bool = try? container.decode(Bool.self) { return bool }
if let int = try? container.decode(Int.self) { return int }
if let int32 = try? container.decode(Int32.self) { return int32 }
if let int64 = try? container.decode(Int64.self) { return int64 }
if let uint = try? container.decode(UInt.self) { return uint }
if let uint32 = try? container.decode(UInt32.self) { return uint32 }
if let uint64 = try? container.decode(UInt64.self) { return uint64 }
if let double = try? container.decode(Double.self) { return double }
if let float = try? container.decode(Float.self) { return float }
if let string = try? container.decode(String.self) { return string }
return nil
}
private static func decodeCollectionValue(from container: SingleValueDecodingContainer) throws -> Any? {
if let array = try? container.decode([AnyCodable].self) {
return array.map { $0.value }
}
if let dictionary = try? container.decode([String: AnyCodable].self) {
return dictionary.mapValues { $0.value }
}
return nil
}
private func encodeValue(_ value: Any, to container: inout SingleValueEncodingContainer) throws -> Bool {
switch value {
case is Void:
try container.encodeNil()
return true
case let primitiveValue:
return try encodePrimitiveValue(primitiveValue, to: &container)
}
}
private func encodePrimitiveValue(_ value: Any, to container: inout SingleValueEncodingContainer) throws -> Bool {
switch value {
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int32 as Int32:
try container.encode(Int(int32))
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let double as Double:
try container.encode(double)
case let float as Float:
try container.encode(float)
case let string as String:
try container.encode(string)
case let point as CGPoint:
try container.encode(["x": point.x, "y": point.y])
case let size as CGSize:
try container.encode(["width": size.width, "height": size.height])
case let url as NSURL:
try container.encode(url.absoluteString)
case let rect as CGRect:
try container.encode(["x": rect.origin.x, "y": rect.origin.y, "width": rect.size.width, "height": rect.size.height])
case let notif as AXNotification:
// AXorcist: Handle AXNotification by encoding its raw string value.
try container.encode(notif.rawValue)
case let attrStr as NSAttributedString:
// AXorcist: Handle NSAttributedString by encoding its string content.
try container.encode(attrStr.string)
case let element as Element:
// AXorcist: Handle AXorcist 'Element' type by encoding its string description as a fallback.
// Prefer specific serialization if a structured JSON representation is needed.
try container.encode(String(describing: element))
case let axEl as AXUIElement:
// AXorcist: Handle CoreFoundation AXUIElement by encoding its string description as a fallback.
// This avoids direct encoding of an opaque CFType.
try container.encode(String(describing: axEl))
case let val where String(describing: type(of: val)) == "__NSCFType":
// Fallback for other __NSCFType instances (CoreFoundation types not explicitly handled).
try container.encode(String(describing: val))
case let array as [AnyCodable]:
try container.encode(array)
case let array as [Any?]:
try container.encode(array.map { AnyCodable($0) })
case let dictionary as [String: AnyCodable]:
try container.encode(dictionary)
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
// DEBUG: Print the type of unhandled values
print("AnyCodable unhandled type: \(String(describing: type(of: value))), value: \(String(describing: value))")
return false
}
return true
}
}

View File

@ -3,6 +3,14 @@
import CoreGraphics // For CGPoint
import Foundation
// Enum for specifying how values, especially for descriptions, should be formatted.
public enum ValueFormatOption: String, Codable, Sendable {
case smart // Tries to provide the most useful, possibly summarized, representation.
case raw // Provides the raw or complete value, potentially verbose.
case textContent // Specifically for text content extraction, might ignore non-textual parts.
case stringified // For detailed string representation, often for logging or debugging.
}
// Main command envelope - REPLACED with definition from axorc.swift for consistency
public struct CommandEnvelope: Codable {
public let commandId: String
@ -12,7 +20,7 @@ public struct CommandEnvelope: Codable {
public let payload: [String: String]? // For ping compatibility
public let debugLogging: Bool
public let locator: Locator? // Locator from this file
public let pathHint: [String]?
public let pathHint: [String]? // This is likely legacy, Locator.rootElementPathHint is preferred
public let maxElements: Int?
public let maxDepth: Int?
public let outputFormat: OutputFormat? // OutputFormat from this file
@ -29,6 +37,11 @@ public struct CommandEnvelope: Codable {
// New field for collectAll filtering
public let filterCriteria: [String: String]?
// Additional fields for various commands
public let includeChildrenBrief: Bool?
public let includeChildrenInText: Bool?
public let includeIgnoredElements: Bool?
enum CodingKeys: String, CodingKey {
case commandId
@ -53,9 +66,12 @@ public struct CommandEnvelope: Codable {
case watchChildren
// CodingKey for new field
case filterCriteria
// Additional CodingKeys
case includeChildrenBrief
case includeChildrenInText
case includeIgnoredElements
}
// Added a public initializer for convenience, matching fields.
public init(commandId: String,
command: CommandType,
application: String? = nil,
@ -72,12 +88,13 @@ public struct CommandEnvelope: Codable {
subCommands: [CommandEnvelope]? = nil,
point: CGPoint? = nil,
pid: Int? = nil,
// Init parameters for observe
notifications: [String]? = nil,
includeElementDetails: [String]? = nil,
watchChildren: Bool? = nil,
// Init parameter for new field
filterCriteria: [String: String]? = nil
filterCriteria: [String: String]? = nil,
includeChildrenBrief: Bool? = nil,
includeChildrenInText: Bool? = nil,
includeIgnoredElements: Bool? = nil
) {
self.commandId = commandId
self.command = command
@ -95,47 +112,137 @@ public struct CommandEnvelope: Codable {
self.subCommands = subCommands
self.point = point
self.pid = pid
// Assignments for observe parameters
self.notifications = notifications
self.includeElementDetails = includeElementDetails
self.watchChildren = watchChildren
// Assignment for new field
self.filterCriteria = filterCriteria
self.includeChildrenBrief = includeChildrenBrief
self.includeChildrenInText = includeChildrenInText
self.includeIgnoredElements = includeIgnoredElements
}
}
// Represents a single criterion for element matching
public struct Criterion: Codable {
public struct Criterion: Codable, Sendable {
public let attribute: String
public let value: String
public let match_type: String? // Match type can be optional, defaulting to exact if nil
public let match_type: JSONPathHintComponent.MatchType? // Retained for flexibility if needed directly in Criterion
public let matchType: JSONPathHintComponent.MatchType? // Preferred name, aliased in custom init/codingkeys if needed
// To handle decoding from either "match_type" or "matchType"
enum CodingKeys: String, CodingKey {
case attribute, value
case match_type // for decoding json
case matchType // for swift code
}
public init(attribute: String, value: String, matchType: JSONPathHintComponent.MatchType? = nil) {
self.attribute = attribute
self.value = value
self.match_type = matchType // Set both to ensure consistency during encoding if old key is used
self.matchType = matchType
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
attribute = try container.decode(String.self, forKey: .attribute)
value = try container.decode(String.self, forKey: .value)
// Try decoding 'matchType' first, then fall back to 'match_type'
if let mt = try container.decodeIfPresent(JSONPathHintComponent.MatchType.self, forKey: .matchType) {
matchType = mt
match_type = mt
} else if let mtOld = try container.decodeIfPresent(JSONPathHintComponent.MatchType.self, forKey: .match_type) {
matchType = mtOld
match_type = mtOld
} else {
matchType = nil
match_type = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(attribute, forKey: .attribute)
try container.encode(value, forKey: .value)
// Encode using the preferred 'matchType' key
try container.encodeIfPresent(matchType, forKey: .matchType)
}
}
/// Represents a step in a hierarchical path, defined by a set of criteria.
public struct PathStep: Codable, Sendable {
public let criteria: [Criterion]
public let matchType: JSONPathHintComponent.MatchType? // How to evaluate criteria (e.g., exact, contains)
public let matchAllCriteria: Bool? // Whether all criteria must match (AND) or any (OR)
public let maxDepthForStep: Int? // Maximum depth to search for this specific step
// CodingKeys to map JSON keys to Swift properties
enum CodingKeys: String, CodingKey {
case criteria
case matchType
case matchAllCriteria
case maxDepthForStep = "max_depth_for_step" // Map JSON's snake_case to Swift's camelCase
}
// Default initializer
public init(criteria: [Criterion],
matchType: JSONPathHintComponent.MatchType? = .exact,
matchAllCriteria: Bool? = true,
maxDepthForStep: Int? = nil) { // Added maxDepthForStep
self.criteria = criteria
self.matchType = matchType
self.matchAllCriteria = matchAllCriteria
self.maxDepthForStep = maxDepthForStep // Initialize
}
/// Returns a string representation suitable for logging
public func descriptionForLog() -> String {
let critDesc = criteria.map { criterion -> String in
"\(criterion.attribute):\(criterion.value)(\((criterion.matchType ?? .exact).rawValue))"
}.joined(separator: ", ")
let depthStringPart: String
if let depth = maxDepthForStep {
depthStringPart = ", Depth: \(depth)"
} else {
depthStringPart = ""
}
let matchTypeStringPart = (matchType ?? .exact).rawValue
let matchAllStringPart = "\(matchAllCriteria ?? true)"
return "[Criteria: (\(critDesc)), MatchType: \(matchTypeStringPart), MatchAll: \(matchAllStringPart)\(depthStringPart)]"
}
}
// Locator for finding elements
public struct Locator: Codable {
public var matchAll: Bool?
public var criteria: [Criterion] // Changed from [String: String] to [Criterion]
public var rootElementPathHint: [JSONPathHintComponent]?
public var descendantCriteria: [String: String]?
public struct Locator: Codable, Sendable {
public var matchAll: Bool? // For the top-level criteria, if path_from_root is not used or fails early.
public var criteria: [Criterion]
public var rootElementPathHint: [JSONPathHintComponent]? // Changed from [PathStep]?
public var descendantCriteria: [String: String]? // This seems to be an older/alternative way? Consider phasing out or clarifying.
public var requireAction: String?
public var computedNameContains: String?
public var debugPathSearch: Bool?
enum CodingKeys: String, CodingKey {
case matchAll
case criteria
case rootElementPathHint
case rootElementPathHint = "path_from_root" // Map to JSON key "path_from_root"
case descendantCriteria
case requireAction
case computedNameContains
case debugPathSearch
}
public init(
matchAll: Bool? = nil,
criteria: [Criterion] = [], // Adjusted default value
rootElementPathHint: [JSONPathHintComponent]? = nil,
matchAll: Bool? = true, // Default to true for criteria
criteria: [Criterion] = [],
rootElementPathHint: [JSONPathHintComponent]? = nil, // Changed from [PathStep]?
descendantCriteria: [String: String]? = nil,
requireAction: String? = nil,
computedNameContains: String? = nil
computedNameContains: String? = nil,
debugPathSearch: Bool? = false
) {
self.matchAll = matchAll
self.criteria = criteria
@ -143,5 +250,411 @@ public struct Locator: Codable {
self.descendantCriteria = descendantCriteria
self.requireAction = requireAction
self.computedNameContains = computedNameContains
self.debugPathSearch = debugPathSearch
}
}
public enum CommandType: String, Codable, Sendable {
case ping
case query
case getAttributes
case describeElement
case getElementAtPoint
case getFocusedElement
case performAction
case batch
case observe
case collectAll
case stopObservation
case isProcessTrusted
case isAXFeatureEnabled
case setFocusedValue // Added from error
case extractText // Added from error
case setNotificationHandler // For AXObserver
case removeNotificationHandler // For AXObserver
case getElementDescription // Utility command for full description
}
public enum OutputFormat: String, Codable, Sendable {
case json
case verbose
case smart // Default, tries to be concise and informative
case jsonString // JSON output as a string, often for AXpector.
case textContent // Specifically for text content output, might ignore non-textual parts.
}
// MARK: - AnyCodable for mixed-type payloads or attributes
// Reverted to simpler AnyCodable with public 'value' to match widespread usage
public struct AnyCodable: Codable, @unchecked Sendable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self.value = ()
} else if let bool = try? container.decode(Bool.self) {
self.value = bool
} else if let int = try? container.decode(Int.self) {
self.value = int
} else if let double = try? container.decode(Double.self) {
self.value = double
} else if let string = try? container.decode(String.self) {
self.value = string
} else if let array = try? container.decode([AnyCodable].self) {
self.value = array.map { $0.value }
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
self.value = dictionary.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if value is () { // Our nil marker for explicit nil
try container.encodeNil()
return
}
switch value {
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
case let array as [Any]:
try container.encode(array.map { AnyCodable($0) })
case let dictionary as [String: Any]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
if let codableValue = value as? Encodable {
// If the value conforms to Encodable, let it encode itself using the provided encoder.
// This is the most flexible approach as the Encodable type can use any container type it needs.
try codableValue.encode(to: encoder)
} else if CFGetTypeID(value as CFTypeRef) == CFNullGetTypeID() {
try container.encodeNil()
} else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "AnyCodable value (\(type(of: value))) cannot be encoded and does not conform to Encodable."))
}
}
}
}
// Helper struct for AnyCodable to properly encode intermediate Encodable values
// This might not be necessary if the direct (value as! Encodable).encode(to: encoder) works.
struct AnyCodablePośrednik<T: Encodable>: Encodable {
let value: T
init(_ value: T) { self.value = value }
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
// Helper protocol to check if a type is Optional
fileprivate protocol OptionalProtocol {
static func isOptional() -> Bool
}
extension Optional: OptionalProtocol {
static func isOptional() -> Bool {
return true
}
}
// MARK: - AXNotificationName enum
// Define AXNotificationName as a String-based enum for notification names
public enum AXNotificationName: String, Codable, Sendable {
case focusedUIElementChanged = "AXFocusedUIElementChanged"
case valueChanged = "AXValueChanged"
case uiElementDestroyed = "AXUIElementDestroyed"
case mainWindowChanged = "AXMainWindowChanged"
case focusedWindowChanged = "AXFocusedWindowChanged"
case applicationActivated = "AXApplicationActivated"
case applicationDeactivated = "AXApplicationDeactivated"
case applicationHidden = "AXApplicationHidden"
case applicationShown = "AXApplicationShown"
case windowCreated = "AXWindowCreated"
case windowResized = "AXWindowResized"
case windowMoved = "AXWindowMoved"
case announcementRequested = "AXAnnouncementRequested"
case focusedApplicationChanged = "AXFocusedApplicationChanged"
case focusedTabChanged = "AXFocusedTabChanged"
case windowMinimized = "AXWindowMiniaturized"
case windowDeminiaturized = "AXWindowDeminiaturized"
case sheetCreated = "AXSheetCreated"
case drawerCreated = "AXDrawerCreated"
case titleChanged = "AXTitleChanged"
case resized = "AXResized"
case moved = "AXMoved"
case created = "AXCreated"
case layoutChanged = "AXLayoutChanged"
case selectedTextChanged = "AXSelectedTextChanged"
case rowCountChanged = "AXRowCountChanged"
case selectedChildrenChanged = "AXSelectedChildrenChanged"
case selectedRowsChanged = "AXSelectedRowsChanged"
case selectedColumnsChanged = "AXSelectedColumnsChanged"
case rowExpanded = "AXRowExpanded"
case rowCollapsed = "AXRowCollapsed"
case selectedCellsChanged = "AXSelectedCellsChanged"
case helpTagCreated = "AXHelpTagCreated"
case loadComplete = "AXLoadComplete"
}
// MARK: - AXCommand and Command Structs
// Enum representing all possible AX commands
public enum AXCommand: Sendable {
case query(QueryCommand)
case performAction(PerformActionCommand)
case getAttributes(GetAttributesCommand)
case describeElement(DescribeElementCommand)
case extractText(ExtractTextCommand)
case batch(AXBatchCommand)
case setFocusedValue(SetFocusedValueCommand)
case getElementAtPoint(GetElementAtPointCommand)
case getFocusedElement(GetFocusedElementCommand)
case observe(ObserveCommand)
case collectAll(CollectAllCommand)
// Computed property to get command type
public var type: String {
switch self {
case .query: return "query"
case .performAction: return "performAction"
case .getAttributes: return "getAttributes"
case .describeElement: return "describeElement"
case .extractText: return "extractText"
case .batch: return "batch"
case .setFocusedValue: return "setFocusedValue"
case .getElementAtPoint: return "getElementAtPoint"
case .getFocusedElement: return "getFocusedElement"
case .observe: return "observe"
case .collectAll: return "collectAll"
}
}
}
// Command envelope for AXorcist
public struct AXCommandEnvelope: Sendable {
public let commandID: String
public let command: AXCommand
public init(commandID: String, command: AXCommand) {
self.commandID = commandID
self.command = command
}
}
// Individual command structs
public struct QueryCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let attributesToReturn: [String]?
public let maxDepthForSearch: Int
public let includeChildrenBrief: Bool?
public init(appIdentifier: String?, locator: Locator, attributesToReturn: [String]? = nil, maxDepthForSearch: Int = 10, includeChildrenBrief: Bool? = nil) {
self.appIdentifier = appIdentifier
self.locator = locator
self.attributesToReturn = attributesToReturn
self.maxDepthForSearch = maxDepthForSearch
self.includeChildrenBrief = includeChildrenBrief
}
}
public struct PerformActionCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let action: String
public let value: AnyCodable?
public let maxDepthForSearch: Int
public init(appIdentifier: String?, locator: Locator, action: String, value: AnyCodable? = nil, maxDepthForSearch: Int = 10) {
self.appIdentifier = appIdentifier
self.locator = locator
self.action = action
self.value = value
self.maxDepthForSearch = maxDepthForSearch
}
}
public struct GetAttributesCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let attributes: [String]
public let maxDepthForSearch: Int
public init(appIdentifier: String?, locator: Locator, attributes: [String], maxDepthForSearch: Int = 10) {
self.appIdentifier = appIdentifier
self.locator = locator
self.attributes = attributes
self.maxDepthForSearch = maxDepthForSearch
}
}
public struct DescribeElementCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let formatOption: ValueFormatOption
public let maxDepthForSearch: Int
public let depth: Int
public let includeIgnored: Bool
public let maxSearchDepth: Int
public init(appIdentifier: String?, locator: Locator, formatOption: ValueFormatOption = .smart, maxDepthForSearch: Int = 10, depth: Int = 3, includeIgnored: Bool = false, maxSearchDepth: Int = 10) {
self.appIdentifier = appIdentifier
self.locator = locator
self.formatOption = formatOption
self.maxDepthForSearch = maxDepthForSearch
self.depth = depth
self.includeIgnored = includeIgnored
self.maxSearchDepth = maxSearchDepth
}
}
public struct ExtractTextCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let maxDepthForSearch: Int
public let includeChildren: Bool?
public let maxDepth: Int?
public init(appIdentifier: String?, locator: Locator, maxDepthForSearch: Int = 10, includeChildren: Bool? = nil, maxDepth: Int? = nil) {
self.appIdentifier = appIdentifier
self.locator = locator
self.maxDepthForSearch = maxDepthForSearch
self.includeChildren = includeChildren
self.maxDepth = maxDepth
}
}
public struct SetFocusedValueCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator
public let value: String
public let maxDepthForSearch: Int
public init(appIdentifier: String?, locator: Locator, value: String, maxDepthForSearch: Int = 10) {
self.appIdentifier = appIdentifier
self.locator = locator
self.value = value
self.maxDepthForSearch = maxDepthForSearch
}
}
public struct GetElementAtPointCommand: Sendable {
public let point: CGPoint
public let appIdentifier: String?
public let pid: Int?
public let x: Float
public let y: Float
public let attributesToReturn: [String]?
public let includeChildrenBrief: Bool?
public init(point: CGPoint, appIdentifier: String? = nil, pid: Int? = nil, attributesToReturn: [String]? = nil, includeChildrenBrief: Bool? = nil) {
self.point = point
self.appIdentifier = appIdentifier
self.pid = pid
self.x = Float(point.x)
self.y = Float(point.y)
self.attributesToReturn = attributesToReturn
self.includeChildrenBrief = includeChildrenBrief
}
public init(appIdentifier: String?, x: Float, y: Float, attributesToReturn: [String]? = nil, includeChildrenBrief: Bool? = nil) {
self.point = CGPoint(x: CGFloat(x), y: CGFloat(y))
self.appIdentifier = appIdentifier
self.pid = nil
self.x = x
self.y = y
self.attributesToReturn = attributesToReturn
self.includeChildrenBrief = includeChildrenBrief
}
}
public struct GetFocusedElementCommand: Sendable {
public let appIdentifier: String?
public let attributesToReturn: [String]?
public let includeChildrenBrief: Bool?
public init(appIdentifier: String?, attributesToReturn: [String]? = nil, includeChildrenBrief: Bool? = nil) {
self.appIdentifier = appIdentifier
self.attributesToReturn = attributesToReturn
self.includeChildrenBrief = includeChildrenBrief
}
}
public struct ObserveCommand: Sendable {
public let appIdentifier: String?
public let locator: Locator?
public let notifications: [String]
public let includeDetails: Bool
public let watchChildren: Bool
public let notificationName: AXNotification
public let includeElementDetails: [String]?
public let maxDepthForSearch: Int
public init(appIdentifier: String?, locator: Locator? = nil, notifications: [String], includeDetails: Bool = true, watchChildren: Bool = false, notificationName: AXNotification, includeElementDetails: [String]? = nil, maxDepthForSearch: Int = 10) {
self.appIdentifier = appIdentifier
self.locator = locator
self.notifications = notifications
self.includeDetails = includeDetails
self.watchChildren = watchChildren
self.notificationName = notificationName
self.includeElementDetails = includeElementDetails
self.maxDepthForSearch = maxDepthForSearch
}
}
// Command struct for collectAll
public struct CollectAllCommand: Sendable {
public let appIdentifier: String?
public let attributesToReturn: [String]?
public let maxDepth: Int
public let filterCriteria: [String: String]? // JSON string for criteria, or can be decoded
public let valueFormatOption: ValueFormatOption?
public init(
appIdentifier: String? = nil, // Provide default nil
attributesToReturn: [String]? = nil,
maxDepth: Int = 10,
filterCriteria: [String: String]? = nil,
valueFormatOption: ValueFormatOption? = .smart
) {
self.appIdentifier = appIdentifier
self.attributesToReturn = attributesToReturn
self.maxDepth = maxDepth
self.filterCriteria = filterCriteria
self.valueFormatOption = valueFormatOption
}
}
// Batch command structures
public struct AXBatchCommand: Sendable {
public struct SubCommandEnvelope: Sendable {
public let commandID: String
public let command: AXCommand
public init(commandID: String, command: AXCommand) {
self.commandID = commandID
self.command = command
}
}
public let commands: [SubCommandEnvelope]
public init(commands: [SubCommandEnvelope]) {
self.commands = commands
}
}
// Alias for backward compatibility if needed
public typealias AXSubCommand = AXCommand
public typealias BatchCommandEnvelope = AXBatchCommand

View File

@ -1,10 +1,70 @@
// Models.swift - Contains core data models and type aliases
import Foundation
import ApplicationServices // Added for AXUIElementGetTypeID
// Type alias for element attributes dictionary
public typealias ElementAttributes = [String: AnyCodable]
// Wrapper for attribute values to make them Codable and handle Any
public struct AXValueWrapper: Codable, Sendable {
public var anyValue: AnyCodable? // This can be nil if the attribute itself had no value or was absent
@MainActor // Added @MainActor to allow calling element.briefDescription
public init(value: Any?) {
let typeOfOriginalValue = String(describing: type(of: value))
axDebugLog("AXVW.init: OrigType='\(typeOfOriginalValue)', Val=\(String(describing: value).prefix(100))")
if let unwrappedValue = value {
let typeOfUnwrappedValue = String(describing: type(of: unwrappedValue))
axDebugLog("AXVW.init: UnwrappedType='\(typeOfUnwrappedValue)'")
if let array = unwrappedValue as? [Any?] {
axDebugLog("AXVW.init: Detected Array. Count: \(array.count)")
// Sanitize each item and wrap the resulting array of sanitized items in AnyCodable
self.anyValue = AnyCodable(array.map { AXValueWrapper.recursivelySanitize($0) })
} else if let dict = unwrappedValue as? [String: Any?] {
axDebugLog("AXVW.init: Detected Dictionary. Count: \(dict.count)")
self.anyValue = AnyCodable(dict.mapValues { AXValueWrapper.recursivelySanitize($0) })
} else {
// Handle single, non-collection items
self.anyValue = AnyCodable(AXValueWrapper.recursivelySanitize(unwrappedValue))
}
} else { // value was nil (absence of value)
axDebugLog("AXVW.init: Original value was nil.")
self.anyValue = nil // The AXValueWrapper's own anyValue property is nil
}
}
// Static helper to sanitize individual items, called recursively by init for collections
@MainActor
private static func recursivelySanitize(_ item: Any?) -> Any { // Returns Any (basic types or () for nil)
guard let anItem = item else { return () } // Convert nil to AnyCodable's nil marker
let cfItem = anItem as CFTypeRef
if CFGetTypeID(cfItem) == CFNullGetTypeID() { return () } // NSNull to AnyCodable's nil
if CFGetTypeID(cfItem) == AXUIElementGetTypeID() { return "<AXUIElement_RS>" }
if let element = anItem as? Element { return "<Element_RS: \(element.briefDescription(option: .raw))>" }
// If it's a collection, recurse. This handles nested collections.
// Note: This recursive call inside a static func might lead to issues if not careful with types.
// However, we are returning basic types or placeholders from the checks above.
if let array = anItem as? [Any?] {
return array.map { recursivelySanitize($0) }
}
if let dict = anItem as? [String: Any?] {
return dict.mapValues { recursivelySanitize($0) }
}
// For basic, already encodable types, return as is.
// This assumes String, Int, Double, Bool are passed through.
return anItem
}
// If AnyCodable has trouble with certain AX types (like AXUIElementRef),
// custom Encodable/Decodable logic might be needed here or in AnyCodable itself.
// For instance, AXUIElementRef might be encoded as a placeholder string or an empty dict.
}
public struct AXElement: Codable, HandlerDataRepresentable {
public var attributes: ElementAttributes?
public var path: [String]?

View File

@ -78,7 +78,7 @@ extension Element {
private func handleEmptyDescription() -> String {
axDebugLog(
"briefDescription: No descriptive attributes found, falling back to underlyingElement description.",
details: ["element": String(describing: self.underlyingElement)]
details: ["element": AnyCodable(String(describing: self.underlyingElement))]
)
return String(describing: self.underlyingElement)
}

View File

@ -58,7 +58,7 @@ extension Element {
} else {
// Use the global axDebugLog helper function for simplicity and correctness
axDebugLog("Failed to get PID for element: \(error.rawValue)",
details: ["element": String(describing: self.underlyingElement)])
details: ["element": AnyCodable(String(describing: self.underlyingElement))])
}
return nil
}

View File

@ -381,4 +381,30 @@ extension Element {
}
return Element.application(for: pid) // application(for:) is sync
}
@MainActor
public static func elementAtPoint(_ point: CGPoint, pid: pid_t) -> Element? {
let appAXUIElement = AXUIElement.application(pid: pid)
guard let elementAXUIElement = AXUIElement.elementAtPosition(in: appAXUIElement, x: Float(point.x), y: Float(point.y)) else {
return nil
}
return Element(elementAXUIElement)
}
// MARK: - Path Generation
@MainActor
public func path() -> Path? {
let pathArray = self.generatePathArray()
return Path(components: pathArray)
}
}
// Path structure to represent element path
public struct Path {
public let components: [String]
public init(components: [String]) {
self.components = components
}
}

View File

@ -1,35 +0,0 @@
// ModelEnums.swift - Contains enum definitions for the AXorcist models
import Foundation
// Enum for output formatting options
public enum OutputFormat: String, Codable {
case smart // Default, tries to be concise and informative
case verbose // More detailed output, includes more attributes/info
case textContent // Primarily extracts textual content
case jsonString // Returns the attributes as a JSON string (new)
}
// Define CommandType enum
public enum CommandType: String, Codable {
case query
case performAction
case getAttributes
case batch
case describeElement
case getFocusedElement
case collectAll
case extractText
case ping
case getElementAtPoint
case observe
case setFocusedValue // New: sets a value on the currently focused element
// Add future commands here, ensuring case matches JSON or provide explicit raw value
}
// Enum for how values should be formatted, especially when dealing with complex types
public enum ValueFormatOption: String, Codable {
case smart // Try to provide the most useful representation (e.g., string for URL, number for size)
case raw // Provide the raw underlying value if possible (e.g., CFTypeRef as String description, or basic data type)
case stringified // Force a string representation of the value
}

View File

@ -2,9 +2,110 @@
import Foundation
// MARK: - AXErrorCode
// Error codes for AXorcist operations
public enum AXErrorCode: String, Codable, Sendable {
case elementNotFound = "element_not_found"
case actionFailed = "action_failed"
case attributeNotFound = "attribute_not_found"
case invalidCommand = "invalid_command"
case unknownCommand = "unknown_command"
case internalError = "internal_error"
case permissionDenied = "permission_denied"
case invalidParameter = "invalid_parameter"
case timeout = "timeout"
case observationFailed = "observation_failed"
case applicationNotFound = "application_not_found"
case batchOperationFailed = "batch_operation_failed"
case actionNotSupported = "action_not_supported"
}
// MARK: - AXResponse
// Main response enum for AXorcist operations
public enum AXResponse: Sendable {
case success(payload: AnyCodable?, logs: [String]?)
case error(message: String, code: AXErrorCode, logs: [String]?)
// Computed property for status
public var status: String {
switch self {
case .success: return "success"
case .error: return "error"
}
}
// Computed property for payload
public var payload: AnyCodable? {
switch self {
case .success(let payload, _): return payload
case .error: return nil
}
}
// Computed property for error
public var error: (message: String, code: AXErrorCode)? {
switch self {
case .success: return nil
case .error(let message, let code, _): return (message, code)
}
}
// Computed property for logs
public var logs: [String]? {
switch self {
case .success(_, let logs): return logs
case .error(_, _, let logs): return logs
}
}
// Static factory methods
public static func successResponse(payload: AnyCodable?, logs: [String]? = nil) -> AXResponse {
return .success(payload: payload, logs: logs)
}
public static func errorResponse(message: String, code: AXErrorCode, logs: [String]? = nil) -> AXResponse {
return .error(message: message, code: code, logs: logs)
}
}
// New protocol for generic data in HandlerResponse
public protocol HandlerDataRepresentable: Codable {}
// Definition for AXElementData based on usage in AXorcist+QueryHandlers.swift
public struct AXElementData: Codable, HandlerDataRepresentable {
public var briefDescription: String?
public var role: String?
public var attributes: [String: AXValueWrapper]? // Assuming AXValueWrapper is Codable
public var allPossibleAttributes: [String]?
public var textualContent: String?
public var childrenBriefDescriptions: [String]?
public var fullAXDescription: String?
// Add path here as it's often part of element data
public var path: [String]?
public init(
briefDescription: String? = nil,
role: String? = nil,
attributes: [String: AXValueWrapper]? = nil,
allPossibleAttributes: [String]? = nil,
textualContent: String? = nil,
childrenBriefDescriptions: [String]? = nil,
fullAXDescription: String? = nil,
path: [String]? = nil
) {
self.briefDescription = briefDescription
self.role = role
self.attributes = attributes
self.allPossibleAttributes = allPossibleAttributes
self.textualContent = textualContent
self.childrenBriefDescriptions = childrenBriefDescriptions
self.fullAXDescription = fullAXDescription
self.path = path
}
}
// Make existing relevant models conform
// AXElement is defined in DataModels.swift, so we'll make it conform there later.
// For now, assume it will.
@ -261,6 +362,59 @@ public struct BatchResponse: Codable {
}
}
// MARK: - Additional Payload Structs
// NoFocusPayload for when no focused element is found
public struct NoFocusPayload: Codable, HandlerDataRepresentable {
public let message: String
public init(message: String) {
self.message = message
}
}
// TextPayload for text extraction
public struct TextPayload: Codable, HandlerDataRepresentable {
public let text: String
public init(text: String) {
self.text = text
}
}
// BatchResponsePayload for batch operations
public struct BatchResponsePayload: Codable, HandlerDataRepresentable {
public let results: [AnyCodable?]?
public let errors: [String]?
public init(results: [AnyCodable?]?, errors: [String]?) {
self.results = results
self.errors = errors
}
}
// MARK: - AXElementDescription
// Structure for element tree descriptions
public struct AXElementDescription: Codable, Sendable {
public let briefDescription: String?
public let role: String?
public let attributes: [String: AXValueWrapper]?
public let children: [AXElementDescription]?
public init(
briefDescription: String?,
role: String?,
attributes: [String: AXValueWrapper]?,
children: [AXElementDescription]? = nil
) {
self.briefDescription = briefDescription
self.role = role
self.attributes = attributes
self.children = children
}
}
// Structure for custom JSON output of handleCollectAll
public struct CollectAllOutput: Codable {
public let commandId: String

View File

@ -1,288 +0,0 @@
// AXorcist+ActionHandlers.swift - Action and data operation handlers
import AppKit
import ApplicationServices
import Darwin
import Foundation
// import Defaults // REMOVED
// import removed - logging utilities now in same module
// MARK: - Action & Data Handlers Extension
extension AXorcist {
// MARK: - Private Helper Methods
@MainActor
private func executeStandardAccessibilityAction(
_ axActionName: CFString,
on targetElement: Element,
actionNameForLog: String
) -> AXError {
let axStatus = AXUIElementPerformAction(targetElement.underlyingElement, axActionName)
if axStatus != .success {
let errorMessage = "[AXorcist.handlePerformAction] Failed to perform " +
"\(actionNameForLog) action: \(axErrorToString(axStatus))"
axErrorLog(errorMessage) // Use global logger
}
return axStatus
}
@MainActor
private func executeSetAttributeValueAction(
attributeName: String,
value: AnyCodable?,
on targetElement: Element
) -> (errorMessage: String?, axStatus: AXError) {
guard let valueToSet = value?.value else {
let errorMsg = "Value for set attribute '\(attributeName)' is nil."
axErrorLog(errorMsg)
return (errorMsg, .cannotComplete)
}
guard targetElement.isAttributeSettable(named: attributeName) else {
let errorMsg = "Attribute '\(attributeName)' is not settable on element \(targetElement.briefDescription(option: ValueFormatOption.smart))."
axErrorLog(errorMsg)
return (errorMsg, .attributeUnsupported)
}
// Convert the value to CFTypeRef if possible, handle different types
var cfValue: CFTypeRef?
if let stringValue = valueToSet as? String {
cfValue = stringValue as CFString
} else if let boolValue = valueToSet as? Bool {
cfValue = (boolValue ? kCFBooleanTrue : kCFBooleanFalse) as CFBoolean
} else if let numberValue = valueToSet as? NSNumber { // Handles Int, Double, etc.
cfValue = numberValue
} else if let objectArray = valueToSet as? [AnyObject] { // If valueToSet is directly [AnyObject]
cfValue = objectArray as CFArray // Then bridge to CFArray
} else if valueToSet is [Any] { // Check if it's [Any] but not caught by [AnyObject] (e.g. array of value types)
// This case is problematic for CFArray which expects objects.
let errorMsg = "Cannot convert array containing non-object types to CFArray for attribute '\(attributeName)'."
axErrorLog(errorMsg)
return (errorMsg, .illegalArgument)
} else {
// For other types, attempt direct casting if it's already a CFTypeRef-compatible type
// This part might need more robust type checking and conversion
// CFGetTypeID(valueToSet as CFTypeRef) != 0 might be a check, but Swift types might not bridge directly
// For simplicity, we'll assume if it's not a common type, it might be problematic.
let errorMsg = "Unsupported value type '\(type(of: valueToSet))' for attribute '\(attributeName)'."
axErrorLog(errorMsg)
return (errorMsg, .illegalArgument)
}
guard let finalCFValue = cfValue else {
let errorMsg = "Failed to convert value for attribute '\(attributeName)' to a compatible CFType."
axErrorLog(errorMsg)
return (errorMsg, .cannotComplete)
}
let axStatus = AXUIElementSetAttributeValue(targetElement.underlyingElement, attributeName as CFString, finalCFValue)
if axStatus != .success {
let errorMsg = "Failed to set attribute '\(attributeName)': \(axErrorToString(axStatus))"
axErrorLog(errorMsg)
return (errorMsg, axStatus)
}
return (nil, .success)
}
// Temporary placeholder until actual definition is found or created
public typealias ActionValueCodable = AnyCodable
// Temporary simple struct for ActionResponse
struct ActionResponse: Codable {
var success: Bool
var message: String?
}
@MainActor
public func handlePerformAction(
for application: String?,
locator: Locator,
actionName: String,
actionValue: AnyCodable? = nil,
maxDepth: Int? = nil
) async -> HandlerResponse {
let logMessage2 = "handlePerformAction: App=\(application ?? AXMiscConstants.focusedApplicationKey), Locator=\(locator), Action=\(actionName), Value=\(String(describing: actionValue))"
axInfoLog(logMessage2)
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
let findResult = await findTargetElement(
for: application ?? AXMiscConstants.focusedApplicationKey,
locator: locator,
maxDepthForSearch: searchMaxDepth
)
if let targetElement = findResult.element {
axDebugLog("handlePerformAction: Element found: \(targetElement.briefDescription(option: ValueFormatOption.smart))")
// Proceed with targetElement
let axStatus: AXError
var actionErrorString: String?
let standardActions = [
AXActionNames.kAXIncrementAction,
AXActionNames.kAXDecrementAction,
AXActionNames.kAXConfirmAction,
AXActionNames.kAXCancelAction,
AXActionNames.kAXShowMenuAction,
AXActionNames.kAXPickAction,
AXActionNames.kAXPressAction,
AXActionNames.kAXRaiseAction
]
if standardActions.contains(actionName) {
axStatus = executeStandardAccessibilityAction(actionName as CFString, on: targetElement, actionNameForLog: actionName)
} else {
let setResult = executeSetAttributeValueAction(attributeName: actionName, value: actionValue, on: targetElement)
axStatus = setResult.axStatus
actionErrorString = setResult.errorMessage
}
if axStatus == .success {
axDebugLog("Action '\(actionName)' performed successfully on \(targetElement.briefDescription(option: ValueFormatOption.smart)).")
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: "", success: true)))
} else {
let finalErrorMessage = actionErrorString ?? "Action '\(actionName)' failed on \(targetElement.briefDescription(option: ValueFormatOption.smart)) with status: \(axErrorToString(axStatus))"
axErrorLog(finalErrorMessage)
return HandlerResponse(error: finalErrorMessage)
}
} else if let errorMsg = findResult.error {
let errorMessage = "handlePerformAction: Error finding element: \(errorMsg)"
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
} else {
// Should not happen if findTargetElement always returns either element or error
let errorMessage = "handlePerformAction: Unknown error finding element."
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
}
}
@MainActor
public func handleExtractText(
for application: String?,
locator: Locator,
maxDepth: Int? = nil
) async -> HandlerResponse {
let logMessage3 = "handleExtractText: App=\(application ?? AXMiscConstants.focusedApplicationKey), Locator=\(locator)"
axInfoLog(logMessage3)
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
let findResult = await findTargetElement(
for: application ?? AXMiscConstants.focusedApplicationKey,
locator: locator,
maxDepthForSearch: searchMaxDepth
)
let appElementInstance = applicationElement(for: application ?? AXMiscConstants.focusedApplicationKey)
if let targetElement = findResult.element {
axDebugLog("handleExtractText: Element found: \(targetElement.briefDescription(option: ValueFormatOption.smart))")
// Proceed with targetElement
guard appElementInstance != nil else {
let appNameToLog = application ?? AXMiscConstants.focusedApplicationKey
let errorMsg = "Could not get application element for path generation in handleExtractText for appKey: \(appNameToLog)."
axErrorLog(errorMsg)
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: errorMsg)
}
var allTextValues: [String] = []
if let title: String = targetElement.attribute(Attribute(AXAttributeNames.kAXTitleAttribute)) { allTextValues.append(title) }
if let desc: String = targetElement.attribute(Attribute(AXAttributeNames.kAXDescriptionAttribute)) { allTextValues.append(desc) }
if let valStr: String = targetElement.attribute(Attribute<String>(AXAttributeNames.kAXValueAttribute)) { allTextValues.append(valStr) }
if let selectedText: String = targetElement.attribute(Attribute(AXAttributeNames.kAXSelectedTextAttribute)) { allTextValues.append(selectedText) }
if let placeholder: String = targetElement.attribute(Attribute(AXAttributeNames.kAXPlaceholderValueAttribute)) { allTextValues.append(placeholder) }
let combinedText = allTextValues.joined(separator: " ").lowercased()
if combinedText.isEmpty {
axDebugLog("No textual content found for element: \(targetElement.briefDescription(option: ValueFormatOption.smart))")
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: nil)), error: "No textual content found")
} else {
axDebugLog("Extracted text: '\(combinedText)' from element: \(targetElement.briefDescription(option: ValueFormatOption.smart))")
return HandlerResponse(data: AnyCodable(TextExtractionResponse(textContent: combinedText.isEmpty ? nil : combinedText)))
}
} else if let errorMsg = findResult.error {
let errorMessage = "handleExtractText: Error finding element: \(errorMsg)"
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
} else {
let errorMessage = "handleExtractText: Unknown error finding element."
axErrorLog(errorMessage)
return HandlerResponse(error: errorMessage)
}
}
// MARK: - Set Focused Value Handler
@MainActor
public func handleSetFocusedValue(
for applicationName: String?,
locator: Locator?, // Optional: to verify the focused element if provided
actionName: String, // Typically kAXValueAttribute or similar
actionValue: AnyCodable? // The value to set
) async -> HandlerResponse {
let appID = applicationName ?? AXMiscConstants.focusedApplicationKey
axInfoLog("[handleSetFocusedValue] App=\(appID), Locator=\(locator != nil ? "criteria: \(locator!.criteria)" : "nil"), Action=\(actionName), Value=\(String(describing: actionValue))")
guard let appElement = applicationElement(for: appID) else {
let errorMsg = "[handleSetFocusedValue] Could not get application element for app: \(appID)"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
guard let focusedElement = appElement.focusedUIElement() else {
let errorMsg = "[handleSetFocusedValue] Could not get focused element for app: \(appID)"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
axDebugLog("[handleSetFocusedValue] Found focused element: \(focusedElement.briefDescription())")
// Optional: Validate against locator if provided
if let aLocator = locator {
// evaluateElementAgainstCriteria is async
let matchStatus = await evaluateElementAgainstCriteria(
element: focusedElement,
locator: aLocator,
actionToVerify: nil, // Not verifying an action here, just the element itself
depth: 0 // Depth is not relevant for a single element check
)
if matchStatus != .fullMatch {
let errorMsg = "[handleSetFocusedValue] Focused element \(focusedElement.briefDescription()) does not match provided locator: criteria=\(aLocator.criteria)"
axWarningLog(errorMsg)
// Depending on strictness, one might return an error here or proceed.
// For now, proceeding but logging a warning.
}
}
guard let valueToSet = actionValue else {
let errorMsg = "[handleSetFocusedValue] Value to set is nil for attribute/action '\(actionName)'."
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
// Use the existing helper for setting attribute values
let setResult = executeSetAttributeValueAction(attributeName: actionName, value: valueToSet, on: focusedElement)
if setResult.axStatus == .success {
axDebugLog("[handleSetFocusedValue] Action/Attribute '\(actionName)' set successfully on focused element \(focusedElement.briefDescription()).")
return HandlerResponse(data: AnyCodable(PerformResponse(commandId: "", success: true)))
} else {
let finalErrorMessage = setResult.errorMessage ?? "[handleSetFocusedValue] Failed to set '\(actionName)' on focused element \(focusedElement.briefDescription()) with status: \(axErrorToString(setResult.axStatus))"
axErrorLog(finalErrorMessage)
return HandlerResponse(error: finalErrorMessage)
}
}
}
// Define PerformResponse and TextExtractionResponse if they are not already globally available
// For now, assuming they are defined elsewhere and are Codable.
// struct PerformResponse: Codable { let commandId: String; let success: Bool }
// struct TextExtractionResponse: Codable { let textContent: String? }
// Removed stub PathHintComponent struct - the canonical one is in SearchCriteriaUtils.swift
// struct PathHintComponent {
// let originalSegment: String?
// }

View File

@ -1,84 +0,0 @@
import ApplicationServices
import Foundation
// GlobalAXLogger is assumed to be available via imports in the main AXorcist module or AXorcistLib
extension AXorcist {
// MARK: - Attribute Access Handlers
/// Checks if a specific attribute of an accessibility element is settable.
///
/// - Parameters:
/// - attributeName: The name of the attribute to check (e.g., kAXValueAttribute).
/// - element: The `AXUIElement` to check.
/// - Returns: `true` if the attribute is settable, `false` otherwise. Returns `false` if the element is nil or on error.
@MainActor
public func isAttributeSettable(
_ attributeName: String,
forElement element: AXUIElement?
) -> Bool {
guard let element = element else {
// Log will only occur if GlobalAXLogger.isCurrentlyCollecting is true
axDebugLog("isAttributeSettable: Element is nil for attribute '\(attributeName)'.")
return false
}
var isSettable: DarwinBoolean = false
let error = AXUIElementIsAttributeSettable(element, attributeName as CFString, &isSettable)
if error != .success {
axDebugLog("isAttributeSettable: Error checking if attribute '\(attributeName)' is settable. Error code: \(error.rawValue)")
return false
}
axDebugLog("isAttributeSettable: Attribute '\(attributeName)' is \(isSettable.boolValue ? "settable" : "not settable").")
return isSettable.boolValue
}
/// Sets the value of a specific attribute for an accessibility element.
///
/// - Parameters:
/// - attributeName: The name of the attribute to set (e.g., kAXValueAttribute).
/// - value: The new value for the attribute. Currently supports String values primarily.
/// Other types might require specific `CFTypeRef` conversion.
/// - element: The `AXUIElement` for which to set the attribute.
/// - Returns: `true` if the attribute was set successfully, `false` otherwise.
@MainActor
public func setAttributeValue(
_ attributeName: String,
to value: Any,
forElement element: AXUIElement?
) -> Bool {
guard let element = element else {
axDebugLog("setAttributeValue: Element is nil for attribute '\(attributeName)'.")
return false
}
let cfValue: CFTypeRef?
if let stringValue = value as? String {
cfValue = stringValue as CFString
} else if let numberValue = value as? NSNumber {
cfValue = numberValue
} else if CFGetTypeID(value as CFTypeRef) != 0 {
cfValue = value as CFTypeRef
} else {
axWarningLog("setAttributeValue: Unsupported value type '\(type(of: value))' for attribute '\(attributeName)'.")
return false
}
guard let finalCFValue = cfValue else {
axWarningLog("setAttributeValue: Failed to convert value '\(value)' to CFTypeRef for attribute '\(attributeName)'.")
return false
}
let error = AXUIElementSetAttributeValue(element, attributeName as CFString, finalCFValue)
if error == .success {
axInfoLog("setAttributeValue: Successfully set attribute '\(attributeName)' to '\(String(describing: value).truncated(to: 100))'.") // Truncate potentially long value
return true
} else {
axErrorLog("setAttributeValue: Error setting attribute '\(attributeName)' to '\(String(describing: value).truncated(to: 100))'. Error code: \(error.rawValue)")
return false
}
}
}

View File

@ -1,242 +0,0 @@
// AXorcist+BatchHandler.swift - Batch processing operations
import AppKit
import ApplicationServices
import CoreGraphics // For CGPoint, potentially used by getElementAtPoint logic
import Foundation
// GlobalAXLogger and AXUtilities are assumed to be available
// MARK: - Batch Processing Handler Extension
extension AXorcist {
@MainActor
private func prepareLocator(for subCommandEnvelope: CommandEnvelope, existingLocator: Locator?) -> Locator? {
guard let newLocator = existingLocator else {
if let oldPathHint = subCommandEnvelope.pathHint, !oldPathHint.isEmpty {
axWarningLog("SubCommand \(subCommandEnvelope.commandId) has a CommandEnvelope.pathHint (old [String] format) but no base locator. This old pathHint will NOT be used. Provide path hints via locator.rootElementPathHint in JSON format.")
}
return existingLocator
}
// If CommandEnvelope.pathHint ([String]?) is provided, AND locator.rootElementPathHint ([JSONPathHintComponent]?) is NOT,
// this indicates an attempt to use the old path hint format. We log a warning as it won't be used by the new system.
if let topLevelOldPathHint = subCommandEnvelope.pathHint,
!topLevelOldPathHint.isEmpty,
newLocator.rootElementPathHint == nil || newLocator.rootElementPathHint!.isEmpty {
axWarningLog("AXorcist+BatchHandler: CommandEnvelope.pathHint (old [String] format) provided for sub-command \(subCommandEnvelope.commandId), but new JSON format (locator.rootElementPathHint) is nil or empty. The old format pathHint will NOT be used. Please update your query to use the new JSON format for rootElementPathHint within the locator object.")
// DO NOT ASSIGN: newLocator.rootElementPathHint = topLevelOldPathHint // This would be a type error
}
return newLocator
}
@MainActor
public func handleBatchCommands(commandEnvelopes: [CommandEnvelope], batchCommandID: String) async -> [HandlerResponse] {
var batchResults: [HandlerResponse] = []
axDebugLog("[AXorcist.handleBatchCommands][BatchID: \(batchCommandID)] Received \(commandEnvelopes.count) sub-commands.")
for subCommandEnvelope in commandEnvelopes {
let result = await processSingleBatchCommand(subCommandEnvelope)
batchResults.append(result)
}
// Batch processing complete
axDebugLog("[AXorcist.handleBatchCommands] Batch processing complete")
axDebugLog("[AXorcist.handleBatchCommands][BatchID: \(batchCommandID)] Completed batch command processing, returning \(batchResults.count) results.")
return batchResults
}
@MainActor
private func processSingleBatchCommand(_ subCommandEnvelope: CommandEnvelope) async -> HandlerResponse {
let subCmdID = subCommandEnvelope.commandId
axDebugLog("[AXorcist.handleBatchCommands][SubCmdID: \(subCmdID)] Processing sub-command: \(subCmdID), type: \(subCommandEnvelope.command)")
// Log operation details
axDebugLog("[AXorcist.handleBatchCommands] Processing sub-command: \(subCmdID) for app: \(subCommandEnvelope.application ?? AXMiscConstants.focusedApplicationKey)")
switch subCommandEnvelope.command {
case .getFocusedElement:
return await processFocusedElementCommand(subCommandEnvelope)
case .getAttributes:
return await processGetAttributes(subCommandEnvelope, subCmdID: subCmdID)
case .query:
return await processQuery(subCommandEnvelope, subCmdID: subCmdID)
case .describeElement:
return await processDescribeElement(subCommandEnvelope, subCmdID: subCmdID)
case .performAction:
return await processPerformAction(subCommandEnvelope, subCmdID: subCmdID)
case .setFocusedValue:
axWarningLog("Command 'setFocusedValue' found in batch. Current batch handler does not specifically process it. Returning as unsupported for now.")
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
case .extractText:
return await processExtractText(subCommandEnvelope, subCmdID: subCmdID)
case .getElementAtPoint:
return await processGetElementAtPoint(subCommandEnvelope, subCmdID: subCmdID)
case .ping:
return processPingCommand(subCmdID)
case .collectAll:
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
case .observe:
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
case .batch:
// Nested batch commands are not supported
axWarningLog("Command 'batch' found within batch. Nested batch commands are not supported.")
return processUnsupportedCommand(subCommandEnvelope, subCmdID: subCmdID)
@unknown default:
return processUnknownCommand(subCommandEnvelope, subCmdID: subCmdID)
}
}
@MainActor
private func processFocusedElementCommand(_ subCommandEnvelope: CommandEnvelope) async -> HandlerResponse {
return await self.handleGetFocusedElement(
for: subCommandEnvelope.application,
requestedAttributes: subCommandEnvelope.attributes
)
}
@MainActor
private func processGetAttributes(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for getAttributes in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleGetAttributes(
for: subCommandEnvelope.application,
locator: finalLocator!,
requestedAttributes: subCommandEnvelope.attributes,
maxDepth: subCommandEnvelope.maxElements,
outputFormat: subCommandEnvelope.outputFormat
)
}
@MainActor
private func processQuery(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for query in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleQuery(
for: subCommandEnvelope.application,
locator: finalLocator!,
maxDepth: subCommandEnvelope.maxElements,
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.outputFormat
)
}
@MainActor
private func processDescribeElement(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for describeElement in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleDescribeElement(
for: subCommandEnvelope.application,
locator: finalLocator!,
maxDepth: subCommandEnvelope.maxDepth,
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.outputFormat
)
}
@MainActor
private func processPerformAction(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for performAction in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
guard let actionName = subCommandEnvelope.actionName else {
let errorMsg = "Action name missing for performAction in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handlePerformAction(
for: subCommandEnvelope.application,
locator: finalLocator!,
actionName: actionName,
actionValue: subCommandEnvelope.actionValue,
maxDepth: subCommandEnvelope.maxElements
)
}
@MainActor
private func processExtractText(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let originalLocator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for extractText in batch (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let finalLocator = prepareLocator(for: subCommandEnvelope, existingLocator: originalLocator)
return await self.handleExtractText(
for: subCommandEnvelope.application,
locator: finalLocator!,
maxDepth: subCommandEnvelope.maxElements
)
}
@MainActor
private func processGetElementAtPoint(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) async -> HandlerResponse {
guard let point = subCommandEnvelope.point else {
let errorMsg = "Missing point for getElementAtPoint command (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
return await self.handleGetElementAtPoint(
for: subCommandEnvelope.application,
point: point,
commandId: subCmdID,
attributesToFetch: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.outputFormat
)
}
@MainActor
private func processPingCommand(_ subCmdID: String) -> HandlerResponse {
let pingMsg = "Ping command handled within batch (sub-command ID: \(subCmdID))"
axInfoLog(pingMsg)
return HandlerResponse(data: nil, error: nil)
}
@MainActor
private func processUnsupportedCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) -> HandlerResponse {
let errorMsg =
"Command type '\(subCommandEnvelope.command)' not supported within batch execution by AXorcist (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
@MainActor
private func processUnknownCommand(_ subCommandEnvelope: CommandEnvelope, subCmdID: String) -> HandlerResponse {
let errorMsg =
"Unknown or unhandled command type '\(subCommandEnvelope.command)' in batch processing within AXorcist (sub-command ID: \(subCmdID))"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
}

View File

@ -1,326 +0,0 @@
// AXorcist+CollectAllHandler.swift - CollectAll operation handler
import AppKit
import ApplicationServices
import Foundation
// Define a new generic Response structure if one doesn't exist suitable for this context.
// For now, we'll assume that a general Response structure is available or defined elsewhere.
// If not, one would be:
public struct ResponseContainer: Codable { // Renamed to avoid conflict if a `Response` type exists elsewhere
public var commandId: String
public var success: Bool
public var command: String // e.g., "collectAll"
public var message: String?
public var data: ResponseData? // Using a new ResponseData enum/struct
public var error: String?
public var debugLogs: [String]?
enum CodingKeys: String, CodingKey {
case commandId = "command_id"
case success
case command
case message
case data
case error
case debugLogs = "debug_logs"
}
}
// Assuming CommandType is an enum with String raw values, e.g.:
// public enum CommandType: String, Codable {
// case query, collectAll, performAction, ping, extractText, batch
// }
// AXElementData is now defined in TreeTraversal.swift
// public struct AXElementData: Codable { ... }
public enum ResponseData: Codable {
case elementsList([AXElementData])
case element(AXElementData?)
case textContent(String?)
case status(String)
case batchResults([ResponseContainer])
}
// MARK: - CollectAll Handler Extension
extension AXorcist {
@MainActor
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 {
axErrorLog("Exception encoding CollectAllOutput: \(error.localizedDescription)")
let cmdId = output.commandId
let cmdType = output.command
let errorJson = """
{"command_id":"\(cmdId)", \
"success":false, \
"command":"\(cmdType)", \
"error_message":"Catastrophic JSON encoding failure for CollectAllOutput. Original error logged.", \
"collected_elements":[], \
"debug_logs":["Catastrophic JSON encoding failure as well."]}
"""
return errorJson
}
}
@MainActor
public func handleCollectAll(
for appIdentifierOrNil: String?,
locator: Locator?,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
commandId: String?,
debugCLI: Bool,
filterCriteria: [String: String]? = nil
) async -> String {
let params = CollectAllParameters(
appIdentifierOrNil: appIdentifierOrNil,
locator: locator,
maxDepth: maxDepth,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat,
commandId: commandId,
focusedAppKey: AXMiscConstants.focusedApplicationKey,
filterCriteria: filterCriteria
)
logCollectAllStart(params)
guard let appElement = applicationElement(for: params.appIdentifier) else {
return createErrorResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
error: "Failed to get app element for identifier: \(params.appIdentifier)",
debugCLI: debugCLI
)
}
let startElementResult = await determineStartElementForCollectAll(
appElement: appElement,
locator: locator,
params: params
)
guard let startElement = startElementResult.element else {
return createErrorResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
error: startElementResult.error ?? "Failed to determine start element for collectAll",
debugCLI: debugCLI
)
}
let collectedElements = await performCollectionTraversal(
startElement: startElement,
appElement: appElement,
params: params
)
return createSuccessResponse(
commandId: params.effectiveCommandId,
appIdentifier: params.appIdentifier,
collectedElements: collectedElements,
debugCLI: debugCLI
)
}
@MainActor
private struct CollectAllParameters {
let effectiveCommandId: String
let appIdentifier: String
let recursionDepthLimit: Int
let attributesToFetch: [String]
let effectiveOutputFormat: OutputFormat
let locator: Locator?
let filterCriteria: [String: String]?
init(
appIdentifierOrNil: String?,
locator: Locator?,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
commandId: String?,
focusedAppKey: String,
filterCriteria: [String: String]?
) {
self.effectiveCommandId = commandId ?? "collectAll_internal_id_\(UUID().uuidString.prefix(8))"
self.appIdentifier = appIdentifierOrNil ?? focusedAppKey
self.recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0)
? maxDepth!
: AXMiscConstants.defaultMaxDepthCollectAll
self.attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch
self.effectiveOutputFormat = outputFormat ?? .smart
self.locator = locator
self.filterCriteria = filterCriteria
}
}
@MainActor
private func logCollectAllStart(_ params: CollectAllParameters) {
let appNameForLog = params.appIdentifier
let locatorCriteriaDesc = params.locator?.criteria.isEmpty == false ? String(describing: params.locator!.criteria) : "nil"
let locatorPathHintDesc = params.locator?.rootElementPathHint?.map { "(attr:\($0.attribute),val:\($0.value),d:\($0.depth ?? -1))" }.joined(separator: " -> ") ?? "nil"
let maxDepthDesc = String(describing: params.recursionDepthLimit)
axInfoLog(
"[AXorcist.handleCollectAll] Starting. App: \(appNameForLog), " +
"LocatorCriteria: \(locatorCriteriaDesc), LocatorJSONPathHint: [\(locatorPathHintDesc)], MaxDepth: \(maxDepthDesc)"
)
axDebugLog(
"Effective recursionDepthLimit: \(params.recursionDepthLimit), " +
"attributesToFetch: \(params.attributesToFetch.count) items, " +
"effectiveOutputFormat: \(params.effectiveOutputFormat.rawValue)"
)
axDebugLog("Using app identifier: \(params.appIdentifier)")
}
@MainActor
private func determineStartElementForCollectAll(
appElement: Element,
locator: Locator?,
params: CollectAllParameters
) async -> (element: Element?, error: String?) {
if let jsonPathComponents = locator?.rootElementPathHint, !jsonPathComponents.isEmpty {
let pathHintDebug = jsonPathComponents.map { "(attr:\($0.attribute),val:\($0.value),d:\($0.depth ?? -1))" }.joined(separator: " -> ")
axDebugLog("[CollectAll] Navigating for start element using JSONPathHint: [\(pathHintDebug)] (\(jsonPathComponents.count) components)")
if let navigatedElement = await navigateToElementByJSONPathHint(
pathHint: jsonPathComponents,
initialSearchElement: appElement
) {
axDebugLog("[CollectAll] JSONPathHint navigation successful. Start element for collectAll: \(navigatedElement.briefDescription())")
return (navigatedElement, nil)
} else {
let errorMsg = "[CollectAll] Failed to navigate to start element using JSONPathHint: [\(pathHintDebug)]"
axWarningLog(errorMsg)
return (nil, errorMsg)
}
} else {
axDebugLog("[CollectAll] No rootElementPathHint (JSON) in locator or locator is nil. Starting collectAll from app root: \(appElement.briefDescription())")
return (appElement, nil)
}
}
@MainActor
private func performCollectionTraversal(
startElement: Element,
appElement: Element,
params: CollectAllParameters
) async -> [AXElementData] {
axDebugLog(
"[CollectAll.performCollectionTraversal] Starting traversal from: \(startElement.briefDescription()), " +
"MaxDepth: \(params.recursionDepthLimit)"
)
// Convert filterCriteria dictionary to [Criterion] if needed
let effectiveLocatorCriteria: [Criterion]
if let locatorCriteria = params.locator?.criteria, !locatorCriteria.isEmpty {
effectiveLocatorCriteria = locatorCriteria
} else if let filterDict = params.filterCriteria, !filterDict.isEmpty {
// Convert dictionary to [Criterion] array
effectiveLocatorCriteria = filterDict.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
}
} else {
effectiveLocatorCriteria = []
}
let matchingLocator = Locator(criteria: effectiveLocatorCriteria)
let collectedData = await collectAllElements(
from: startElement,
matching: matchingLocator,
appElementForContext: appElement,
attributesToFetch: params.attributesToFetch,
outputFormat: params.effectiveOutputFormat,
maxElements: AXMiscConstants.defaultMaxElementsToCollect,
maxSearchDepth: params.recursionDepthLimit
)
axDebugLog("[CollectAll.performCollectionTraversal] Traversal complete. Collected \(collectedData.count) AXElementData items.")
return collectedData
}
@MainActor
private func createErrorResponse(
commandId: String,
appIdentifier: String,
error: String,
debugCLI: Bool
) -> String {
let logs = debugCLI ? axGetLogsAsStrings(format: .text) : nil
let output = CollectAllOutput(
commandId: commandId,
success: false,
command: "collectAll",
collectedElements: nil,
appIdentifier: appIdentifier,
debugLogs: logs,
message: error
)
return encode(output)
}
@MainActor
private func createSuccessResponse(
commandId: String,
appIdentifier: String,
collectedElements: [AXElementData],
debugCLI: Bool
) -> String {
let logs = debugCLI ? axGetLogsAsStrings(format: .text) : nil
let output = CollectAllOutput(
commandId: commandId,
success: true,
command: "collectAll",
collectedElements: collectedElements,
appIdentifier: appIdentifier,
debugLogs: logs,
message: "Successfully collected \(collectedElements.count) elements."
)
return encode(output)
}
}
// Ensure navigateToElementByPathHint is accessible. It is private in ElementSearch.swift.
// For this refactor, we'll assume it's made internal or public, or we use findTargetElement.
// The call `AXorcist.collectAll` in `performCollectionTraversal` refers to the global func in ElementSearch.swift
// It needs to be `ElementSearch.collectAll` or just `collectAll` if in the same module and accessible.
// For the edit, I will assume `collectAll` is callable as a static/global function.
// And `navigateToElementByPathHint` is also made accessible for `determineStartElementForCollectAll`.
// To make `navigateToElementByPathHint` and `collectAll` (from ElementSearch) accessible here,
// they should be marked `internal` or `public` in ElementSearch.swift if AXorcist+CollectAllHandler.swift
// is in a different file but same module, or `public` if different modules.
// For simplicity of this step, I'm writing the logic as if they are callable.
// Assuming CollectAllOutput and ErrorDetails structs are defined appropriately.
// Removed duplicate definition of CollectAllOutput, it's defined in Core/ResponseModels.swift
// public struct CollectAllOutput: Codable {
// public let commandId: String
// public let success: Bool
// public let command: String // e.g. "collectAll"
// public var collectedElements: [AXElementData] = []
// public var errorMessage: String?
// public var debugLogs: [String]?
// public var errorDetails: ErrorDetails?
//
// enum CodingKeys: String, CodingKey {
// case commandId = "command_id"
// case success
// case command
// case collectedElements = "collected_elements"
// case errorMessage = "error_message"
// case debugLogs = "debug_logs"
// case errorDetails = "error_details"
// }
// }
// public struct ErrorDetails: Codable {
// public var code: Int? // e.g., AXError raw value or a custom error code
// public var domain: String? // e.g., "AXorcist.AXErrorDomain"
// public var context: String? // Additional context about the error
// }

View File

@ -1,203 +0,0 @@
import AppKit // For pid_t, AXObserver, AXUIElement, kAXFocusedUIElementChangedNotification etc.
import ApplicationServices
// GlobalAXLogger is used instead of OSLog
// MARK: - Focus Tracking Handlers
extension AXorcist {
// Public typealias for the callback
public typealias AXFocusChangeCallback = @MainActor (_ focusedElement: Element, _ pid: pid_t, _ notification: AXNotification) -> Void
// MARK: - New Implementation using AXObserverCenter
@MainActor
public func startFocusTracking(
for pid: pid_t,
callback: @escaping AXFocusChangeCallback
) -> Bool {
axDebugLog("Attempting to start focus tracking for PID \(pid).")
// Stop existing tracking if any
if self.focusTrackingPID != 0 || self.focusedUIElementToken != nil || self.focusedWindowToken != nil {
axInfoLog("Focus tracking potentially active (PID \(self.focusTrackingPID), UI token: \(self.focusedUIElementToken != nil), Window token: \(self.focusedWindowToken != nil)). Stopping first.")
_ = stopFocusTracking() // Ensure any previous tracking is fully stopped
}
self.focusTrackingPID = pid
self.focusTrackingCallback = callback
var success = true
// Subscribe to Focused UI Element Changed
let focusedUIElementResult = AXObserverCenter.shared.subscribe(
pid: pid,
notification: .focusedUIElementChanged
) { [weak self] eventPid, axNotification, rawElement, _ in
guard let self = self, let cb = self.focusTrackingCallback else {
axWarningLog("Focus tracking callback or self is nil for .focusedUIElementChanged. PID: \(eventPid)")
return
}
let focusedElement = Element(rawElement)
cb(focusedElement, eventPid, axNotification)
axDebugLog("Focus tracking: .focusedUIElementChanged. Element: \(focusedElement.briefDescription()), PID: \(eventPid), Notification: \(axNotification.rawValue)")
}
if case .success(let token) = focusedUIElementResult {
self.focusedUIElementToken = token
axInfoLog("Successfully subscribed to .focusedUIElementChanged for PID \(pid). Token: \(token.id)")
} else if case .failure(let error) = focusedUIElementResult {
axErrorLog("Failed to subscribe to .focusedUIElementChanged for PID \(pid). Error: \(error.localizedDescription)")
success = false
}
// Subscribe to Focused Window Changed
let focusedWindowResult = AXObserverCenter.shared.subscribe(
pid: pid,
notification: .focusedWindowChanged
) { [weak self] eventPid, axNotification, rawWindowElement, nsUserInfo in
guard let self = self, let cb = self.focusTrackingCallback else {
axWarningLog("Focus tracking callback or self is nil for .focusedWindowChanged. PID: \(eventPid)")
return
}
let windowElement = Element(rawWindowElement)
var actualFocusedElement: Element = windowElement
if let uiInfo = nsUserInfo, let focusedCF = uiInfo[AXMiscConstants.focusedUIElementKey] {
if CFGetTypeID(focusedCF as CFTypeRef) == AXUIElementGetTypeID() {
actualFocusedElement = Element(focusedCF as! AXUIElement)
} else {
axWarningLog("userInfo contained kAXFocusedUIElementKey but it was not an AXUIElement. Type: \(CFGetTypeID(focusedCF as CFTypeRef))")
}
} else if let focusedEl = windowElement.focusedUIElement() {
actualFocusedElement = focusedEl
}
cb(actualFocusedElement, eventPid, axNotification)
axDebugLog("Focus tracking: .focusedWindowChanged. Actual Element: \(actualFocusedElement.briefDescription()), PID: \(eventPid), Notification: \(axNotification.rawValue)")
}
if case .success(let token) = focusedWindowResult {
self.focusedWindowToken = token
axInfoLog("Successfully subscribed to .focusedWindowChanged for PID \(pid). Token: \(token.id)")
} else if case .failure(let error) = focusedWindowResult {
axErrorLog("Failed to subscribe to .focusedWindowChanged for PID \(pid). Error: \(error.localizedDescription)")
success = false
}
if success {
axInfoLog("Successfully started focus tracking for PID \(pid).")
} else {
axErrorLog("Error starting focus tracking for PID \(pid). Cleaning up.")
_ = stopFocusTracking() // Clean up any partial subscriptions
}
return success
}
@MainActor
public func stopFocusTracking() -> Bool {
guard self.focusTrackingPID != 0 || self.focusedUIElementToken != nil || self.focusedWindowToken != nil || self.systemWideFocusToken != nil else {
axInfoLog("Focus tracking not active (no PID or tokens).")
return true
}
axInfoLog("Attempting to stop focus tracking for PID \(self.focusTrackingPID). UI Token: \(self.focusedUIElementToken != nil), Window Token: \(self.focusedWindowToken != nil), System Token: \(self.systemWideFocusToken != nil)")
var allSuccess = true
if let token = self.focusedUIElementToken {
do {
try AXObserverCenter.shared.unsubscribe(token: token)
axInfoLog("Unsubscribed from .focusedUIElementChanged. PID: \(self.focusTrackingPID)")
self.focusedUIElementToken = nil
} catch {
axErrorLog("Failed to unsubscribe from .focusedUIElementChanged for PID \(self.focusTrackingPID): \(error)")
allSuccess = false
}
}
if let token = self.focusedWindowToken {
do {
try AXObserverCenter.shared.unsubscribe(token: token)
axInfoLog("Unsubscribed from .focusedWindowChanged. PID: \(self.focusTrackingPID)")
self.focusedWindowToken = nil
} catch {
axErrorLog("Failed to unsubscribe from .focusedWindowChanged for PID \(self.focusTrackingPID): \(error)")
allSuccess = false
}
}
if let token = self.systemWideFocusToken {
do {
try AXObserverCenter.shared.unsubscribe(token: token)
axInfoLog("Unsubscribed from system-wide focus tracking.")
self.systemWideFocusToken = nil
} catch {
axErrorLog("Failed to unsubscribe from system-wide focus tracking: \(error)")
allSuccess = false
}
}
self.focusTrackingPID = 0 // Reset PID regardless of unsubscribe success
self.focusTrackingCallback = nil
// focusTrackingObserver is not used with AXObserverCenter
if allSuccess {
axInfoLog("Successfully stopped all focus tracking subscriptions.")
} else {
axWarningLog("Encountered errors while stopping focus tracking subscriptions.")
}
return allSuccess
}
// MARK: - System-wide Focus Tracking
@MainActor
public func startSystemWideFocusTracking(callback: @escaping AXFocusChangeCallback) -> Bool {
axDebugLog("Attempting to start system-wide focus tracking.")
// Stop existing tracking if any
if self.systemWideFocusToken != nil {
axInfoLog("System-wide focus tracking already active. Stopping first.")
_ = stopFocusTracking() // stopFocusTracking will handle systemWideFocusToken
}
// Ensure other PID-specific tracking is also stopped
if self.focusTrackingPID != 0 {
axInfoLog("PID-specific focus tracking active (PID \(self.focusTrackingPID)). Stopping it as well.")
_ = stopFocusTracking()
}
self.focusTrackingCallback = callback // Store the callback
let systemWideResult = AXObserverCenter.shared.subscribe(
pid: nil, // System-wide
notification: .focusedApplicationChanged
) { [weak self] eventPid, axNotification, rawAppElement, _ in
guard let self = self, let cb = self.focusTrackingCallback else {
axWarningLog("System-wide focus tracking callback or self is nil for .focusedApplicationChanged. PID: \(eventPid)")
return
}
let appElement = Element(rawAppElement)
var actualFocusedElement: Element = appElement
if let focusedUI = appElement.focusedUIElement() {
actualFocusedElement = focusedUI
}
cb(actualFocusedElement, eventPid, axNotification)
axDebugLog("System-wide focus tracking: .focusedApplicationChanged. Actual Element: \(actualFocusedElement.briefDescription()), PID: \(eventPid), Notification: \(axNotification.rawValue)")
}
switch systemWideResult {
case .success(let token):
self.systemWideFocusToken = token
axInfoLog("Successfully subscribed to system-wide .focusedApplicationChanged. Token: \(token.id)")
return true
case .failure(let error):
axErrorLog("Failed to subscribe to system-wide .focusedApplicationChanged. Error: \(error.localizedDescription)")
return false
}
}
@MainActor
public func stopSystemWideFocusTracking() -> Bool {
axDebugLog("Attempting to stop system-wide focus tracking (will call general stopFocusTracking).")
// stopFocusTracking() already handles the systemWideFocusToken
return stopFocusTracking()
}
}

View File

@ -1,93 +0,0 @@
// AXorcist+GetElementAtPointHandler.swift - Handler for GetElementAtPoint command
import Foundation
import ApplicationServices // For CGPoint
extension AXorcist {
@MainActor
public func handleGetElementAtPoint(
for application: String?, // Optional: application context
point: CGPoint, // The screen coordinates
commandId: String?,
attributesToFetch: [String]? = nil, // Optional: attributes to fetch for the found element
outputFormat: OutputFormat? = .smart,
valueFormatOption: ValueFormatOption? = .smart, // Assuming ValueFormatOption exists
debugCLI: Bool = false
) async -> HandlerResponse {
let effectiveCommandId = commandId ?? "getElementAtPoint_\(UUID().uuidString.prefix(8))"
axInfoLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] App=\(application ?? "systemWide"), Point=(\(point.x), \(point.y))")
// 1. Determine the root element for hit-testing
// If an application is specified, use that application's main window or element.
// Otherwise, use the system-wide element.
var searchRootElement: Element?
if let appIdentifier = application {
searchRootElement = applicationElement(for: appIdentifier)
if searchRootElement == nil {
let errorMsg = "Application not found: \(appIdentifier)"
axErrorLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] \(errorMsg)")
return HandlerResponse(data: nil, error: errorMsg)
}
} else {
searchRootElement = Element.systemWide()
}
guard let rootElement = searchRootElement else {
// This case should ideally not be reached if the above logic is sound
let errorMsg = "Could not determine root element for hit-testing."
axErrorLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] \(errorMsg)")
return HandlerResponse(data: nil, error: errorMsg)
}
// 2. Perform the hit-test
var hitElementRef: AXUIElement?
let error = AXUIElementCopyElementAtPosition(rootElement.underlyingElement, Float(point.x), Float(point.y), &hitElementRef)
if error != .success {
let errorMsg = "AXUIElementCopyElementAtPosition failed: \(axErrorToString(error))"
axErrorLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] \(errorMsg)")
return HandlerResponse(data: nil, error: errorMsg)
}
guard let foundAxElement = hitElementRef else {
let errorMsg = "No element found at point (\(point.x), \(point.y))"
axInfoLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] \(errorMsg)")
// Not necessarily an error, could be empty space. Return success with no data.
return HandlerResponse(data: nil, error: nil)
}
let foundElement = Element(foundAxElement)
axInfoLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] Found element: \(foundElement.briefDescription(option: .smart))")
// 3. Fetch attributes if requested (similar to handleQuery)
let (attributes, attrErrors) = await getElementAttributes(
element: foundElement,
attributes: attributesToFetch ?? AXorcist.defaultAttributesToFetch,
outputFormat: outputFormat ?? .smart,
valueFormatOption: valueFormatOption ?? .smart
)
if !attrErrors.isEmpty {
axWarningLog("[AXorcist.handleGetElementAtPoint][CmdID: \(effectiveCommandId)] Errors fetching attributes: \(attrErrors.map { $0.message }.joined(separator: "; "))")
}
let appElementForPath = applicationElement(for: application ?? AXMiscConstants.focusedApplicationKey)
let elementData = AXElementData(
path: foundElement.generatePathArray(upTo: appElementForPath),
attributes: attributes,
role: attributes[AXAttributeNames.kAXRoleAttribute]?.value as? String,
computedName: foundElement.computedName()
)
return HandlerResponse(data: AnyCodable(elementData), error: nil)
}
}
// Need to ensure ValueFormatOption is defined, if not already.
// For now, assuming it exists, e.g., in ModelEnums.swift or similar.
// public enum ValueFormatOption: String, Codable {
// case smart
// case raw
// case stringified
// }

View File

@ -1,357 +0,0 @@
// AXorcist+QueryHandlers.swift - Query and search operation handlers
import AppKit
import ApplicationServices
import Foundation
// GlobalAXLogger is assumed to be available
// Define arrow separator constant for joining path hints
// private let arrowSeparator = " -> " // No longer needed here
// MARK: - Query & Search Handlers Extension
extension AXorcist {
// MARK: - handleQuery
@MainActor
public func handleQuery(
for appIdentifierOrNil: String?,
locator: Locator,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?
) async -> HandlerResponse {
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
axDebugLog("Handling query for app: \(appIdentifier), locator: \(locator)",
file: #file, function: #function, line: #line)
// findTargetElement is sync
let findResult = await findTargetElement(
for: appIdentifier,
locator: locator,
maxDepthForSearch: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
)
guard let foundElement = findResult.element else {
return HandlerResponse(
data: nil,
error: findResult.error ?? "Element not found by handleQuery."
)
}
// applicationElement is sync
guard let appElement = applicationElement(for: appIdentifier) else {
axErrorLog("Application not found for path context: \(appIdentifier)")
return await buildQueryResponse(
element: foundElement,
appElement: nil,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat
)
}
return await buildQueryResponse(
element: foundElement,
appElement: appElement,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat
)
}
// Helper: Navigate with path hint if provided - REMOVED as findTargetElement handles this via locator
/*
@MainActor
private func navigateWithPathHintIfNeeded(
appElement: Element,
pathHint: [String]?
) -> (element: Element?, error: String?) {
// ... implementation removed ...
}
*/
// Helper: Find element with locator - REMOVED as findTargetElement handles this
/*
@MainActor
private func findElementWithLocator(
locator: Locator,
effectiveElement: Element,
appElement: Element,
maxDepth: Int?
) -> (element: Element?, error: String?) {
// ... implementation removed ...
}
*/
// Helper: Find search start element - REMOVED as findTargetElement handles this
/*
@MainActor
private func findSearchStartElement(
locator: Locator,
effectiveElement: Element,
appElement: Element
) -> (element: Element?, error: String?) {
// ... implementation removed ...
}
*/
// Helper: Build query response - made internal to be accessible by other handlers
@MainActor
internal func buildQueryResponse(
element: Element,
appElement: Element?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?
) async -> HandlerResponse {
let (attributes, _) = await getElementAttributes(
element: element,
attributes: requestedAttributes ?? [],
outputFormat: outputFormat ?? .smart
)
// element.generatePathArray is sync
let axElement = AXElement(
attributes: attributes,
path: element.generatePathArray(upTo: appElement)
)
return HandlerResponse(
data: AnyCodable(axElement),
error: nil
)
}
// MARK: - handleGetAttributes
@MainActor
public func handleGetAttributes(
for appIdentifierOrNil: String?,
locator: Locator,
requestedAttributes: [String]?,
maxDepth: Int?,
outputFormat: OutputFormat?
) async -> HandlerResponse {
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
axDebugLog("Handling getAttributes for app: \(appIdentifier), locator: \(locator)",
file: #file, function: #function, line: #line)
// findTargetElement is sync
let findResult = await findTargetElement(
for: appIdentifier,
locator: locator,
maxDepthForSearch: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
)
guard let foundElement = findResult.element else {
return HandlerResponse(
data: nil,
error: findResult.error ?? "Element not found by handleGetAttributes."
)
}
let (attributes, _) = await getElementAttributes(
element: foundElement,
attributes: requestedAttributes ?? AXorcist.defaultAttributesToFetch,
outputFormat: outputFormat ?? .smart
)
let axElementData = AXElement(attributes: attributes, path: nil)
return HandlerResponse(data: AnyCodable(axElementData), error: nil)
}
// MARK: - handleDescribeElement
@MainActor
public func handleDescribeElement(
for appIdentifierOrNil: String?,
locator: Locator,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?
) async -> HandlerResponse {
let appIdentifier = appIdentifierOrNil ?? AXMiscConstants.focusedApplicationKey
axDebugLog("Handling describeElement for app: \(appIdentifier), locator: \(locator)",
file: #file, function: #function, line: #line)
let searchMaxDepth = AXMiscConstants.defaultMaxDepthSearch
// findTargetElement is sync
let findResult = await findTargetElement(
for: appIdentifier,
locator: locator,
maxDepthForSearch: searchMaxDepth
)
guard let foundElement = findResult.element else {
return HandlerResponse(
data: nil,
error: findResult.error ?? "Element not found by handleDescribeElement."
)
}
// applicationElement is sync
guard let appElement = applicationElement(for: appIdentifier) else {
axErrorLog("Application not found for path context in describeElement: \(appIdentifier)")
return HandlerResponse(error: "Application \(appIdentifier) not found for describeElement context.")
}
let descriptionTreeMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthDescribe
// describeElementTree is async
let elementTree = await describeElementTree(
element: foundElement,
appElement: appElement,
maxDepth: descriptionTreeMaxDepth,
currentDepth: 0,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat ?? .smart
)
return HandlerResponse(data: AnyCodable(elementTree), error: nil)
}
@MainActor
internal func describeElementTree(
element: Element,
appElement: Element,
maxDepth: Int,
currentDepth: Int,
requestedAttributes: [String]?,
outputFormat: OutputFormat
) async -> AXElementNode {
let (attributes, _) = await getElementAttributes(
element: element,
attributes: requestedAttributes ?? AXorcist.defaultAttributesToFetch,
outputFormat: outputFormat
)
// element.generatePathArray is sync
let pathArray = element.generatePathArray(upTo: appElement)
var childrenNodes: [AXElementNode]?
if currentDepth < maxDepth {
// element.children is sync
if let children = element.children() {
childrenNodes = await withTaskGroup(of: AXElementNode.self) { group in
for childElement in children {
group.addTask {
await self.describeElementTree(
element: childElement,
appElement: appElement,
maxDepth: maxDepth,
currentDepth: currentDepth + 1,
requestedAttributes: requestedAttributes,
outputFormat: outputFormat
)
}
}
var results: [AXElementNode] = []
for await node in group {
results.append(node)
}
return results
}
}
}
return AXElementNode(attributes: attributes, path: pathArray, children: childrenNodes)
}
// Gets attributes of an element, handling errors and output format.
// This function itself doesn't need to be async if its callees (element.attribute, element.computedName) are sync.
// This is an internal helper, potentially an instance method if it uses instance state,
// or could be a static/global utility if it doesn't.
// Renaming to avoid conflict with the global getElementAttributes.
@MainActor
internal func fetchInstanceElementAttributes(element: Element, attributes names: [String], outputFormat: OutputFormat) -> (attributes: [String: AnyCodable], errors: [String]) {
var fetchedAttributes: [String: AnyCodable] = [:]
var errors: [String] = []
var effectiveAttributeNames = names
if names.contains("*") || names.contains("all") {
// element.attributeNames() is sync
effectiveAttributeNames = element.attributeNames() ?? []
// Ensure some defaults if wildcard is used
let defaults: Set<String> = [AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXTitleAttribute, AXAttributeNames.kAXRoleDescriptionAttribute]
effectiveAttributeNames.append(contentsOf: defaults.filter { !effectiveAttributeNames.contains($0) })
}
// Always try to include a few key attributes for identification if not present
let minimumDefaults: Set<String> = [AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXTitleAttribute, "computedName"]
for defaultAttr in minimumDefaults {
if !effectiveAttributeNames.contains(defaultAttr) {
effectiveAttributeNames.append(defaultAttr)
}
}
for name in effectiveAttributeNames {
if name == "computedName" { // Handle pseudo-attribute
// element.computedName() is sync
if let computed = element.computedName() {
fetchedAttributes[name] = AnyCodable(computed)
} else {
// Optionally represent nil or skip
}
continue
}
// element.attribute() is sync
if let value = element.attribute(Attribute<Any>(name)) { // Use Attribute<Any> for generic fetching
fetchedAttributes[name] = AnyCodable(value)
} else {
// errors.append("Attribute '\(name)' not found or nil.") // Optionally log errors for missing attributes
}
}
return (fetchedAttributes, errors)
}
}
// Define AXElementNode for describeElement output (if not already defined)
// This struct represents a node in the described element tree.
public struct AXElementNode: Codable, HandlerDataRepresentable {
public var attributes: ElementAttributes?
public var path: [String]?
public var children: [AXElementNode]? // Recursive definition for children
public init(attributes: ElementAttributes?, path: [String]? = nil, children: [AXElementNode]? = nil) {
self.attributes = attributes
self.path = path
self.children = children
}
}
// Helper function to get an application element - can be shared
// This function is already available globally as applicationElement(for:)
/*
@MainActor
internal func getApplicationElement(for identifier: String) -> Element? {
return applicationElement(for: identifier)
}
*/
// Removed navigateToElement, as findTargetElement (and its internal findElementViaPathAndCriteria)
// now handles path navigation based on locator.rootElementPathHint.
// findTargetElement should be a global function in ElementSearch.swift or similar,
// not part of AXorcist extension, to be callable by various handlers.
// For now, assuming it's accessible. It takes 'for' (appID), 'locator', 'maxDepthForSearch'.
// The pathHint parameter for findTargetElement will be removed in its own definition.
/**
Placeholder for the global findTargetElement function.
Its actual implementation is in ElementSearch.swift and will be modified.
This is just to satisfy the compiler for this file's changes.
*/
/*
@MainActor
internal func findTargetElement(
for appIdentifier: String?,
locator: Locator,
maxDepthForSearch: Int
) async -> (element: Element?, error: String?) {
// Actual implementation will be in ElementSearch.swift
// This will use applicationElement(for:) and then findElementViaPathAndCriteria
// using locator (which includes locator.rootElementPathHint).
return (nil, "findTargetElement not yet fully refactored here")
}
*/
// The definition of findTargetElement needs to be adjusted in ElementSearch.swift
// Note: getElementAttributes is already a global helper.
// `search` method is part of AXorcist or ElementSearch.

View File

@ -1,147 +0,0 @@
import AppKit // Required for NSScreen, CGPoint, pid_t
import ApplicationServices
import CoreGraphics // Ensure CGPoint is available for parameter type
import Foundation
// GlobalAXLogger should be available
// MARK: - Utility Handlers
extension AXorcist {
/// Fetches basic identifying information (role and title) for a given accessibility element.
/// This is useful for creating preview strings for elements referenced by attributes.
///
/// - Parameters:
/// - element: The `AXUIElement` to get information for.
/// - Returns: A tuple containing the preview string (or nil) and an array of debug log messages.
@MainActor
public func getPreviewString(
forElement axElement: AXUIElement?
) -> String? {
guard let axUIElement = axElement else {
axDebugLog("getPreviewString: Element is nil.")
return nil
}
let element = Element(axUIElement)
let role = element.role()
let title = element.title()
axDebugLog("getPreviewString: Fetched Role=\'\(role ?? "<nil>")\', Title=\'\(title ?? "<nil>")\' for element.")
if let roleValue = role, !roleValue.isEmpty, let titleValue = title, !titleValue.isEmpty {
return "\(roleValue): \(titleValue)"
} else if let roleValue = role, !roleValue.isEmpty {
return roleValue
} else if let titleValue = title, !titleValue.isEmpty {
return titleValue
} else {
return "<AXUIElement>"
}
}
/// Gets the accessibility element at a given screen point for a specific application.
///
/// - Parameters:
/// - point: The screen point to hit-test. Its coordinate system origin (bottom-left or top-left)
/// is determined by `isScreenCoordinatesTopLeft`.
/// - pid: The process identifier of the target application.
/// - isScreenCoordinatesTopLeft: If `true`, `point` is assumed to have (0,0) at top-left of the screen.
/// If `false` (default), `point` is assumed to be AppKit-style (bottom-left is (0,0))
/// and will be converted internally.
/// - isDebugLoggingEnabled: Whether to enable debug logging.
/// - currentDebugLogs: An inout array for debug logs.
/// - Returns: An `AXUIElement` if found, otherwise `nil`. The caller is responsible for releasing this element if non-nil.
@MainActor
public func getElementAtPoint(
pid explicitPID: pid_t? = nil,
point: CGPoint,
appIdentifierOrNil: String? = nil, // Can be bundle ID or app name
requestedAttributes: [String]? = nil,
isScreenCoordinatesTopLeft: Bool = false
) async -> HandlerResponse {
var targetPID: pid_t = 0
var appElementForPath: Element?
if let explicitPID = explicitPID, explicitPID > 0 {
targetPID = explicitPID
axDebugLog("getElementAtPoint: Using explicit PID: \(targetPID)")
} else if let appID = appIdentifierOrNil {
if let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: appID).first {
targetPID = runningApp.processIdentifier
axDebugLog("getElementAtPoint: Found running app by bundle ID '\(appID)', PID: \(targetPID)")
} else if let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: appID.contains(".app") ? appID : "\(appID).app").first { // Try adding .app
targetPID = runningApp.processIdentifier
axDebugLog("getElementAtPoint: Found running app by bundle ID '\(appID).app', PID: \(targetPID)")
} else if let pidInt = Int(appID), let runningApp = NSRunningApplication(processIdentifier: pid_t(pidInt)) {
targetPID = runningApp.processIdentifier // Validates PID and gets consistent pid_t
axDebugLog("getElementAtPoint: Using PID from appIdentifier string '\(appID)': \(targetPID)")
} else {
// Fallback to finding app by name (less reliable)
let apps = NSWorkspace.shared.runningApplications.filter { $0.localizedName?.localizedCaseInsensitiveContains(appID) == true || $0.bundleIdentifier == appID }
if let firstMatch = apps.first {
targetPID = firstMatch.processIdentifier
axDebugLog("getElementAtPoint: Found app by name/fallback '\(appID)', PID: \(targetPID)")
} else {
let errorMsg = "getElementAtPoint: Could not determine PID from appIdentifier: \(appID)"
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
}
} else { // No explicitPID and no appIdentifierOrNil, try focused app
guard let focusedApp = NSWorkspace.shared.frontmostApplication else {
let errorMsg = "getElementAtPoint: No PID or appIdentifier provided, and could not get focused application."
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
targetPID = focusedApp.processIdentifier
axDebugLog("getElementAtPoint: No PID/appIdentifier, using focused app PID: \(targetPID) (\(focusedApp.localizedName ?? "Unknown"))")
}
if targetPID == 0 { // Still no valid PID
let errorMsg = "getElementAtPoint: Failed to resolve a valid application PID."
axErrorLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let systemWideAppElement = AXUIElementCreateApplication(targetPID)
appElementForPath = Element(systemWideAppElement) // For path generation later
var finalY = Float(point.y)
if !isScreenCoordinatesTopLeft {
guard let mainScreen = NSScreen.main else {
let errorMsg = "getElementAtPoint: Cannot get main screen info for coordinate conversion."
axWarningLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
let screenHeight = Float(mainScreen.frame.height)
finalY = screenHeight - Float(point.y)
axDebugLog("getElementAtPoint: Converted point from (\(point.x), \(point.y)) to (\(point.x), \(finalY)) for AX top-left system.")
} else {
axDebugLog("getElementAtPoint: Using provided point (\(point.x), \(finalY)) as top-left screen coordinates.")
}
var hitTestElementRef: AXUIElement?
let error = AXUIElementCopyElementAtPosition(systemWideAppElement, Float(point.x), finalY, &hitTestElementRef)
if error == .success, let rawElement = hitTestElementRef {
let foundElement = Element(rawElement)
axDebugLog("getElementAtPoint: Successfully found element at (\(Float(point.x))), \(finalY)). Element: \(foundElement.briefDescription())")
// Call the global getElementAttributes function
let (attributes, _) = await getElementAttributes(
element: foundElement,
attributes: requestedAttributes ?? AXorcist.defaultAttributesToFetch,
outputFormat: .jsonString, // Using jsonString format
valueFormatOption: .smart // Assuming default options
)
let pathArray = appElementForPath != nil ? foundElement.generatePathArray(upTo: appElementForPath!) : foundElement.generatePathArray()
let axElement = AXElement(attributes: attributes, path: pathArray)
return HandlerResponse(data: AnyCodable(axElement))
} else {
let errorMsg = "getElementAtPoint: No element found at (\(Float(point.x))), \(finalY)). Error: \(error.rawValue)"
axDebugLog(errorMsg)
return HandlerResponse(error: errorMsg)
}
}
}

View File

@ -8,6 +8,19 @@ public enum AXLogLevel: String, Codable, Sendable, CaseIterable {
case critical // For errors that might lead to a crash or critical malfunction
}
// Added AXLogDetailLevel
public enum AXLogDetailLevel: String, Codable, Sendable, CaseIterable {
case minimal // Only critical/error messages
case normal // Info, warning, error, critical
case verbose // Debug, info, warning, error, critical (all messages)
}
// Added AXLogOutputFormat
public enum AXLogOutputFormat: String, Codable, Sendable, CaseIterable {
case text
case json
}
public struct AXLogEntry: Codable, Sendable, Identifiable {
public let id: UUID
public let timestamp: Date
@ -16,7 +29,7 @@ public struct AXLogEntry: Codable, Sendable, Identifiable {
public let file: String?
public let function: String?
public let line: Int?
public let details: [String: String]? // Optional dictionary for structured details
public let details: [String: AnyCodable]? // Changed to AnyCodable
public init(
id: UUID = UUID(),
@ -26,7 +39,7 @@ public struct AXLogEntry: Codable, Sendable, Identifiable {
file: String? = #file,
function: String? = #function,
line: Int? = #line,
details: [String: String]? = nil
details: [String: AnyCodable]? = nil // Changed to AnyCodable
) {
self.id = id
self.timestamp = timestamp
@ -71,7 +84,19 @@ extension AXLogEntry {
logParts.append("- \(message)")
if let details = details, !details.isEmpty {
logParts.append("Details: \(details.map { key, value in "\(key): \(value)" }.joined(separator: ", "))")
// Simplified details formatting for AnyCodable
let detailString = details.map { key, value in
let valueStr: String
if let v = value.value as? String {
valueStr = v
} else if let v = value.value as? CustomStringConvertible {
valueStr = v.description
} else {
valueStr = String(describing: value.value)
}
return "\(key): \(valueStr)"
}.joined(separator: ", ")
logParts.append("Details: [\(detailString)]")
}
return logParts.joined(separator: " ")

View File

@ -1,128 +0,0 @@
import Foundation
// Extension to GlobalAXLogger for additional functionality needed by the command system
extension GlobalAXLogger {
// Properties for tracking state
private static var _isLoggingEnabled: Bool = false
private static var _detailLevel: AXLogDetailLevel = .normal
private static var _currentCommandID: String?
private static var _currentAppName: String?
// MARK: - Logging Control
public func isLoggingEnabled() async -> Bool {
return Self._isLoggingEnabled
}
public func setLoggingEnabled(_ enabled: Bool) async {
Self._isLoggingEnabled = enabled
}
public func getDetailLevel() async -> AXLogDetailLevel {
return Self._detailLevel
}
public func setDetailLevel(_ level: AXLogDetailLevel) async {
Self._detailLevel = level
}
// MARK: - Operation Context
public func updateOperationDetails(commandID: String?, appName: String?) async {
Self._currentCommandID = commandID
Self._currentAppName = appName
}
// MARK: - Log Formatting
public func getLogsAsStrings(
format: AXLogOutputFormat,
includeTimestamps: Bool = true,
includeLevels: Bool = true,
includeDetails: Bool = false,
includeAppName: Bool = false,
includeCommandID: Bool = false
) async -> [String] {
let entries = GlobalAXLogger.shared.getEntries()
return entries.map { entry in
var components: [String] = []
if includeTimestamps {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
components.append("[\(formatter.string(from: entry.timestamp))]")
}
if includeLevels {
components.append("[\(entry.level.rawValue.uppercased())]")
}
if includeCommandID, let commandID = Self._currentCommandID {
components.append("[CMD:\(commandID)]")
}
if includeAppName, let appName = Self._currentAppName {
components.append("[APP:\(appName)]")
}
components.append(entry.message)
if includeDetails, let details = entry.details, !details.isEmpty {
let detailsStr = details.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
components.append("{\(detailsStr)}")
}
return components.joined(separator: " ")
}
}
internal func getLogEntriesAsJSON() async throws -> String {
let entries = GlobalAXLogger.shared.getEntries()
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(entries)
return String(data: jsonData, encoding: .utf8) ?? "[]"
}
public func getLogsAsJSON() async throws -> String {
return try await getLogEntriesAsJSON()
}
public func getLogsAsStringsIfEnabled(
format: AXLogOutputFormat,
includeTimestamps: Bool = true,
includeLevels: Bool = true,
includeDetails: Bool = false,
includeAppName: Bool = false,
includeCommandID: Bool = false
) async -> [String]? {
if await self.isLoggingEnabled() {
return await self.getLogsAsStrings(
format: format,
includeTimestamps: includeTimestamps,
includeLevels: includeLevels,
includeDetails: includeDetails,
includeAppName: includeAppName,
includeCommandID: includeCommandID
)
}
return nil
}
}
// MARK: - Log Detail Level
public enum AXLogDetailLevel: String, Codable {
case minimal
case normal
case verbose
}
// MARK: - Log Output Format
public enum AXLogOutputFormat: String, Codable {
case text
case json
}

View File

@ -20,6 +20,9 @@ public class GlobalAXLogger {
// Callers must ensure main-thread execution for all logger interactions.
public var isJSONLoggingEnabled: Bool = false // Direct access, assuming main-thread safety
// Instance properties for logging control, moved from extension
public var isLoggingEnabled: Bool = false
public var detailLevel: AXLogDetailLevel = .normal
private init() {
if let envVar = ProcessInfo.processInfo.environment["AXORC_JSON_LOG_ENABLED"], envVar.lowercased() == "true" {
@ -31,6 +34,13 @@ public class GlobalAXLogger {
// MARK: - Logging Core
// Assumes this method is always called on the main thread.
public func log(_ entry: AXLogEntry) {
guard self.isLoggingEnabled else { return }
// Use fully qualified enum cases
if entry.level == .debug && self.detailLevel != AXLogDetailLevel.verbose {
if self.detailLevel == AXLogDetailLevel.minimal { return }
if self.detailLevel == AXLogDetailLevel.normal && entry.level == .debug { return }
}
let condensedMessage: String = {
if entry.message.count > maxMessageLength {
let prefix = entry.message.prefix(maxMessageLength)
@ -128,27 +138,27 @@ public class GlobalAXLogger {
// MARK: - Global Logging Functions (Convenience Wrappers)
// These are synchronous and assume GlobalAXLogger.shared.log is safe to call directly (i.e., from main thread).
public func axDebugLog(_ message: String, details: [String: String]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
public func axDebugLog(_ message: String, details: [String: AnyCodable]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
let entry = AXLogEntry(level: .debug, message: message, file: file, function: function, line: line, details: details)
GlobalAXLogger.shared.log(entry)
}
public func axInfoLog(_ message: String, details: [String: String]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
public func axInfoLog(_ message: String, details: [String: AnyCodable]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
let entry = AXLogEntry(level: .info, message: message, file: file, function: function, line: line, details: details)
GlobalAXLogger.shared.log(entry)
}
public func axWarningLog(_ message: String, details: [String: String]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
public func axWarningLog(_ message: String, details: [String: AnyCodable]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
let entry = AXLogEntry(level: .warning, message: message, file: file, function: function, line: line, details: details)
GlobalAXLogger.shared.log(entry)
}
public func axErrorLog(_ message: String, details: [String: String]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
public func axErrorLog(_ message: String, details: [String: AnyCodable]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
let entry = AXLogEntry(level: .error, message: message, file: file, function: function, line: line, details: details)
GlobalAXLogger.shared.log(entry)
}
public func axFatalLog(_ message: String, details: [String: String]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
public func axFatalLog(_ message: String, details: [String: AnyCodable]? = nil, file: String = #file, function: String = #function, line: Int = #line) {
let entry = AXLogEntry(level: .critical, message: message, file: file, function: function, line: line, details: details)
GlobalAXLogger.shared.log(entry)
}

View File

@ -48,6 +48,21 @@ extension HandlerResponse {
}
}
// MARK: - AXResponse Integration
extension HandlerResponse {
/// Creates a HandlerResponse from an AXResponse
/// - Parameter axResponse: The AXResponse to convert
public init(from axResponse: AXResponse) {
switch axResponse {
case .success(let payload, _):
self.init(data: payload, error: nil)
case .error(let message, _, _):
self.init(data: nil, error: message)
}
}
}
// MARK: - Error Structure for Detailed Errors (Example)
/// An example structure for providing more detailed error information if needed.

View File

@ -9,6 +9,9 @@ public struct JSONPathHintComponent: Codable, Sendable {
case exact
case contains
case regex
case containsAny
case prefix
case suffix
}
/// The type of attribute to match (e.g., "ROLE", "TITLE", "DOM", "DOMCLASS"). Case-insensitive.

View File

@ -2,9 +2,11 @@
import ApplicationServices
import Foundation
// GlobalAXLogger is assumed available
import Logging
// GlobalAXLogger, AXMiscConstants, JSONPathHintComponent are assumed available.
// JSONPathHintComponent is in Models/JSONPathHintComponent.swift
// Added logger definition
private let logger = Logger(label: "AXorcist.ElementSearch")
// MARK: - Main Element Finding Orchestration
@ -17,308 +19,292 @@ public func findTargetElement(
for appIdentifier: String,
locator: Locator,
maxDepthForSearch: Int
) async -> (element: Element?, error: String?) {
) -> (element: Element?, error: String?) {
let pathHintDebugString = locator.rootElementPathHint?.map { component in "(attr: \\(component.attribute), val: \\(component.value), depth: \\(component.depth ?? -1))" }.joined(separator: "; ") ?? "nil"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"[findTargetElement ENTRY] App=\\(appIdentifier), Locator: criteria=\(locator.criteria), " +
"jsonPathHint=[\\(pathHintDebugString)]"
))
guard let appElement = applicationElement(for: appIdentifier) else {
let msg = "Application not found: \\(appIdentifier)"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: msg))
return (nil, msg)
}
let jsonPathComponents = locator.rootElementPathHint ?? []
let hasOnlyAppSpecificCriteria = locator.criteria.allSatisfy { criterion in
let key = criterion.attribute.lowercased()
return key == "pid" || key == "bundleid" || key == "appname"
}
if !jsonPathComponents.isEmpty && hasOnlyAppSpecificCriteria {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "findTargetElement: Using JSONPathHint primarily as criteria are app-specific or empty."))
if let elementFromPath = await navigateToElementByJSONPathHint(
pathHint: jsonPathComponents,
initialSearchElement: appElement
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "findTargetElement: Found element directly via JSONPathHint: \\(elementFromPath.briefDescription(option: .smart))"))
if let descCrit = locator.descendantCriteria, !descCrit.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "findTargetElement: Performing descendantCriteria search. DescCriteria: \\(descCrit)"))
// Convert descendantCriteria dictionary to [Criterion]
let descCriteria = descCrit.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
}
let descLocator = Locator(criteria: descCriteria)
if let descendant = await traverseAndSearch(currentElement: elementFromPath,
locator: descLocator,
maxDepth: maxDepthForSearch) {
return (descendant, nil)
} else {
return (nil, "Element found by path hint, but descendant criteria did not match.")
}
}
return (elementFromPath, nil)
} else {
let msg = "Element not found via JSONPathHint: [\\(pathHintDebugString)]"
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: msg))
return (nil, msg)
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "findTargetElement: Proceeding with criteria-based search (JSONPathHint may refine start)."))
if let foundElement = await findElementViaCriteriaAndJSONPathHint(
application: appElement,
locator: locator,
maxDepth: maxDepthForSearch
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "findTargetElement: Found via criteria (and/or JSONPathHint): \\(foundElement.briefDescription(option: .smart))"))
var baseElement = foundElement
if let descCrit = locator.descendantCriteria, !descCrit.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "findTargetElement: Performing descendantCriteria search. DescCriteria: \\(descCrit)"))
// Convert descendantCriteria dictionary to [Criterion]
let descCriteria = descCrit.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
}
let descLoc = Locator(criteria: descCriteria)
if let descendant = await traverseAndSearch(currentElement: baseElement,
locator: descLoc,
maxDepth: maxDepthForSearch) {
baseElement = descendant
} else {
let msg = "Descendant element not found matching: \\(descCrit)"
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: msg))
return (nil, msg)
}
}
return (baseElement, nil)
let pathHintDebugString = locator.rootElementPathHint?.map { $0.descriptionForLog() }.joined(separator: "\n -> ") ?? "nil"
let criteriaDebugString = locator.criteria.map { criterion in "[\(criterion.attribute):\(criterion.value), match:\(criterion.matchType?.rawValue ?? "exact")]" }.joined(separator: ", ")
// Use criteriaDebugString in the log message
var logMessage = """
FindTargetEl: START
App: '\(appIdentifier)'
MaxDepth: \(maxDepthForSearch)
"""
if !criteriaDebugString.isEmpty {
logMessage += "\n Initial Criteria: \(criteriaDebugString)"
} else {
let msg = "Element not found matching criteria: \\(locator.criteria)"
if !jsonPathComponents.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "\\(msg) (JSONPathHint was: [\\(pathHintDebugString)])"))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: msg))
}
return (nil, msg)
logMessage += "\n Initial Criteria: none"
}
}
logMessage += "\n PathHint (count: \(locator.rootElementPathHint?.count ?? 0)):\n -> \(pathHintDebugString)"
logger.info("\(logMessage)")
// MARK: - Core Search Logic
guard let appElement = getApplicationElement(for: appIdentifier) else {
logger.error("FindTargetEl: Could not get application element for \(appIdentifier)")
return (nil, "Application not found or not accessible: \(appIdentifier)")
}
@MainActor
internal func navigateToElementByJSONPathHint(pathHint: [JSONPathHintComponent], initialSearchElement: Element) async -> Element? {
var currentElementInPath = initialSearchElement
let pathHintDesc = pathHint.map { component in "(attr:\\(component.attribute),val:\\(component.value),d:\\(component.depth ?? -1))" }.joined(separator: " -> ")
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"navigateToElementByJSONPathHint: Starting with \\(pathHint.count) JSON components [\\(pathHintDesc)] from " +
"\\(initialSearchElement.briefDescription(option: .smart))"
))
var currentSearchElement = appElement
var searchStartingPointDescription = "application root \(appElement.briefDescription(option: .smart))"
for (index, component) in pathHint.enumerated() {
guard let componentCriteria = component.simpleCriteria else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "navigateToElementByJSONPathHint: Skipping component #\\(index) (attr:\\(component.attribute)) due to invalid/unresolved attribute type. Path broken."))
return nil
// 1. Navigate by pathHint if provided
if let jsonPathComponents = locator.rootElementPathHint, !jsonPathComponents.isEmpty {
logger.debug("FindTargetEl: Path hint provided with \(jsonPathComponents.count) components. Navigating path first from \(searchStartingPointDescription).")
// Convert [JSONPathHintComponent] to [PathStep]
let pathSteps: [PathStep] = jsonPathComponents.map { component in
let criterion = Criterion(attribute: component.attribute, value: component.value, matchType: component.matchType)
return PathStep(criteria: [criterion], matchType: component.matchType, matchAllCriteria: true, maxDepthForStep: component.depth)
}
let depthForThisStep = component.depth ?? JSONPathHintComponent.defaultDepthForSegment
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"navigateToElementByJSONPathHint: Processing component #\\(index) {attr:\"\\(component.attribute)\", val:\"\\(component.value)\", depth:\\(depthForThisStep)} " +
"starting from \\(currentElementInPath.briefDescription(option: .raw))."
))
if let foundElementForThisStep = await findDescendantMatchingCriteria(
startElement: currentElementInPath,
criteria: componentCriteria,
matchType: component.matchType ?? .exact,
maxDepthForThisHintStep: depthForThisStep
if let navigatedElement = findDescendantAtPath(
currentRoot: currentSearchElement,
pathComponents: pathSteps, // Use converted pathSteps
maxDepth: maxDepthForSearch, // Path navigation steps might need their own depth concept or use overall
debugSearch: locator.debugPathSearch ?? false
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "navigateToElementByJSONPathHint: Matched component #\\(index). Found: \\(foundElementForThisStep.briefDescription(option: .raw)). Advancing."))
currentElementInPath = foundElementForThisStep
logger.info("FindTargetEl: Path navigation successful. New search root: \(navigatedElement.briefDescription(option: ValueFormatOption.smart))")
currentSearchElement = navigatedElement
searchStartingPointDescription = "navigated path element \(currentSearchElement.briefDescription(option: ValueFormatOption.smart))"
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "navigateToElementByJSONPathHint: Failed component #\\(index) {attr:\"\\(component.attribute)\", val:\"\\(component.value)\"} from \\(currentElementInPath.briefDescription(option: .raw)) depth \\(depthForThisStep). Path broken."))
return nil
let pathFailedError = "FindTargetEl: Path navigation failed. Could not find element at specified path hint: [\(pathHintDebugString)]"
logger.warning("\(pathFailedError)")
return (nil, pathFailedError)
}
} else {
logger.debug("FindTargetEl: No path hint provided, or path hint was empty. Searching from \(searchStartingPointDescription).")
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "navigateToElementByJSONPathHint: Successfully navigated all \\(pathHint.count) JSON components. Final element: \\(currentElementInPath.briefDescription(option: .smart))"))
return currentElementInPath
}
@MainActor
internal func findDescendantMatchingCriteria(startElement: Element, criteria: [String: String], matchType: JSONPathHintComponent.MatchType, maxDepthForThisHintStep: Int) async -> Element? {
guard !criteria.isEmpty else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "findDescendantMatchingCriteria: Called with empty criteria."))
return nil
}
// Convert dictionary criteria to [Criterion]
let criterionArray = criteria.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
}
let tempLocator = Locator(criteria: criterionArray)
return await traverseAndSearch(currentElement: startElement, locator: tempLocator, maxDepth: maxDepthForThisHintStep)
}
@MainActor
internal func traverseAndSearch(currentElement: Element, locator: Locator, maxDepth: Int) async -> Element? {
// 2. After path navigation (or if no path), apply final criteria from locator.criteria
// If locator.criteria is empty, it means the path navigation itself was meant to find the target.
if locator.criteria.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "traverseAndSearch: Called with empty criteria. Returning current element: \\(currentElement.briefDescription(option: .smart))"))
return currentElement
if locator.rootElementPathHint?.isEmpty ?? true {
let noCriteriaError = "FindTargetEl: No criteria provided in locator and no path hint. Cannot perform search."
logger.error("\(noCriteriaError)")
return (nil, noCriteriaError)
}
logger.info("FindTargetEl: Path hint was used and no further criteria specified. Returning element found at path: \(currentSearchElement.briefDescription(option: .smart))")
return (currentSearchElement, nil)
}
let visitor = SearchVisitor(locator: locator)
var traverser = TreeTraverser()
var traversalState = TraversalState(maxDepth: maxDepth, startElement: currentElement)
return await traverser.traverse(from: currentElement, visitor: visitor, state: &traversalState)
}
@MainActor
internal func processPathHintAndDetermineStartElement(application: Element, locator: Locator) async -> Element {
guard let jsonPathComponents = locator.rootElementPathHint, !jsonPathComponents.isEmpty else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "processPathHint: No rootElementPathHint (JSON) provided or empty. Searching from app root."))
return application
}
let pathHintDebug = jsonPathComponents.map { component in "(attr:\\(component.attribute),val:\\(component.value),d:\\(component.depth ?? -1))" }.joined(separator: " -> ")
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "processPathHint: Starting JSON path hint navigation [\\(pathHintDebug)] for search root. \\(jsonPathComponents.count) components."))
if let elementFromPathHint = await navigateToElementByJSONPathHint(
pathHint: jsonPathComponents,
initialSearchElement: application
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "processPathHint: JSON path hint navigation successful. New search start: \\(elementFromPathHint.briefDescription(option: .smart))."))
return elementFromPathHint
logger.debug("FindTargetEl: Applying final criteria from locator (\(locator.criteria.count) criteria) starting from \(searchStartingPointDescription). MatchAll=\(locator.matchAll ?? true), MatchType=\(locator.criteria.first?.matchType?.rawValue ?? "default/exact")")
// Use matchAll and matchType from the main Locator object for these final criteria, if they exist there.
// Otherwise, SearchVisitor will use its defaults or what's on individual Criterion objects.
let finalSearchMatchType = locator.criteria.first?.matchType ?? .exact // Simplified: take from first criterion or default
let finalSearchMatchAll = locator.matchAll ?? true
let searchVisitor = SearchVisitor(
criteria: locator.criteria,
matchType: finalSearchMatchType,
matchAllCriteria: finalSearchMatchAll,
stopAtFirstMatch: true, // For the final search, we typically want the first match.
maxDepth: maxDepthForSearch
)
traverseAndSearch(element: currentSearchElement, visitor: searchVisitor, currentDepth: 0, maxDepth: maxDepthForSearch)
if let foundMatch = searchVisitor.foundElement { // Changed from foundElements.first
logger.info("FindTargetEl: Found final descendant matching criteria: \(foundMatch.briefDescription(option: .smart))")
return (foundMatch, nil)
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "processPathHint: JSON path hint navigation failed [\\(pathHintDebug)]. Full search will be from app root."))
return application
let criteriaDesc = locator.criteria.map { "\($0.attribute):\($0.value)" }.joined(separator: ", ")
let finalSearchError = "FindTargetEl: No element found matching final criteria [\(criteriaDesc)] starting from \(searchStartingPointDescription)."
logger.warning("\(finalSearchError)")
return (nil, finalSearchError)
}
}
@MainActor
internal func findElementViaCriteriaAndJSONPathHint(
application: Element,
locator: Locator,
maxDepth: Int
) async -> Element? {
let pathHintForLog = locator.rootElementPathHint?.map { component in "(attr:\\(component.attribute),val:\\(component.value),d:\\(component.depth ?? -1))" }.joined(separator: " -> ") ?? "nil"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"[findElementViaCriteriaAndJSONPathHint ENTRY] AppPID: \\(application.pid() ?? -1), Locator.criteria: \\(locator.criteria), " +
"JSONPathHint: [\\(pathHintForLog)]"
))
let searchStartElement = await processPathHintAndDetermineStartElement(application: application, locator: locator)
if locator.criteria.isEmpty && (locator.rootElementPathHint != nil && !locator.rootElementPathHint!.isEmpty) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[findElementViaCriteriaAndJSONPathHint] Criteria empty, JSON path hint used. Returning: \\(searchStartElement.briefDescription(option: .smart))"))
return searchStartElement
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[findElementViaCriteriaAndJSONPathHint] Search start: \\(searchStartElement.briefDescription(option: .smart)). Applying criteria: \\(locator.criteria)"))
return await traverseAndSearch(
currentElement: searchStartElement,
locator: locator,
maxDepth: maxDepth
)
}
// MARK: - Element Collection
// MARK: - Element Collection Logic
@MainActor
internal func collectAllElements(
public func collectAllElements(
from startElement: Element,
matching locator: Locator,
appElementForContext: Element,
attributesToFetch: [String],
outputFormat: OutputFormat,
maxElements: Int?,
maxSearchDepth: Int
) async -> [AXElementData] {
let effectiveFilterCriteria = locator.criteria
let visitor = CollectAllVisitor(
attributesToFetch: attributesToFetch,
outputFormat: outputFormat,
appElement: appElementForContext,
filterCriteria: effectiveFilterCriteria
)
var traverser = TreeTraverser()
var traversalState = TraversalState(maxDepth: maxSearchDepth, startElement: startElement)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "CollectAll: Starting from \\(startElement.briefDescription(option: .smart)), FilterCriteria: \\(effectiveFilterCriteria), MaxDepth: \\(maxSearchDepth)"))
matching criteria: [Criterion]? = nil,
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch,
includeIgnored: Bool = false
) -> [Element] {
let criteriaDebugString = criteria?.map { "\($0.attribute):\($0.value)(\($0.match_type?.rawValue ?? "exact"))" }.joined(separator: ", ") ?? "all"
logger.info("CollectAll: From [\(startElement.briefDescription(option: ValueFormatOption.smart))], Criteria: [\(criteriaDebugString)], MaxDepth: \(maxDepth), Ignored: \(includeIgnored)")
_ = await traverser.traverse(from: startElement, visitor: visitor, state: &traversalState)
let visitor = CollectAllVisitor(criteria: criteria, includeIgnored: includeIgnored)
traverseAndSearch(element: startElement, visitor: visitor, currentDepth: 0, maxDepth: maxDepth)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "CollectAll: Visitor collected \\(visitor.collectedElements.count) AXElementData items."))
if let maxEl = maxElements, visitor.collectedElements.count > maxEl {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "CollectAll: Truncating to \\(maxEl) elements."))
return Array(visitor.collectedElements.prefix(maxEl))
}
logger.info("CollectAll: Found \(visitor.collectedElements.count) elements.")
return visitor.collectedElements
}
enum ElementMatchStatus {
case fullMatch
case partialMatchActionMissing
case noMatch
// MARK: - Generic Tree Traversal with Visitor
// Protocol for visitors used in tree traversal
@MainActor
public protocol ElementVisitor {
// If visit returns .stop, traversal stops. If .skipChildren, children of current element are not visited.
// Otherwise, traversal continues (.continue).
func visit(element: Element, depth: Int) -> TreeVisitorResult
}
public enum TreeVisitorResult {
case `continue`
case skipChildren
case stop
}
@MainActor
internal func evaluateElementAgainstCriteria(
public func traverseAndSearch(
element: Element,
locator: Locator,
actionToVerify: String?,
depth: Int
) async -> ElementMatchStatus {
if await !elementMatchesCriteria(element, criteria: locator.criteria) {
return .noMatch
visitor: ElementVisitor,
currentDepth: Int,
maxDepth: Int
) {
if currentDepth > maxDepth {
logger.debug("Traverse: Max depth \(maxDepth) reached at [\(element.briefDescription(option: ValueFormatOption.smart))]. Stopping this branch.")
return
}
if let actionName = actionToVerify, !actionName.isEmpty {
if element.isActionSupported(actionName) {
return .fullMatch
} else {
return .partialMatchActionMissing
let visitResult = visitor.visit(element: element, depth: currentDepth)
switch visitResult {
case .stop:
logger.debug("Traverse: Visitor requested STOP at [\(element.briefDescription(option: ValueFormatOption.smart))] depth \(currentDepth).")
return
case .skipChildren:
logger.debug("Traverse: Visitor requested SKIP_CHILDREN at [\(element.briefDescription(option: ValueFormatOption.smart))] depth \(currentDepth).")
return // Do not process children
case .continue:
logger.debug("Traverse: Visitor requested CONTINUE at [\(element.briefDescription(option: ValueFormatOption.smart))] depth \(currentDepth). Processing children.")
// Continue to process children
break
}
if let children = element.children() {
for child in children {
traverseAndSearch(element: child, visitor: visitor, currentDepth: currentDepth + 1, maxDepth: maxDepth)
// If the visitor is a SearchVisitor that stops at first match, check if it found something.
if let searchVisitor = visitor as? SearchVisitor, searchVisitor.stopAtFirstMatchInternal, searchVisitor.foundElement != nil {
logger.debug("Traverse: SearchVisitor found match and stopAtFirstMatch is true. Stopping traversal early.")
return // Stop traversal early
}
}
}
return .fullMatch
}
// MARK: - Search Visitor Implementation
@MainActor
public func search(element: Element,
locator: Locator,
requireAction: String?,
depth: Int = 0,
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) async -> Element? {
var traverser = TreeTraverser()
let visitor = SearchVisitor(locator: locator, requireAction: requireAction)
var state = TraversalState(maxDepth: maxDepth, startElement: element)
let result = await traverser.traverse(from: element, visitor: visitor, state: &state)
return result
public class SearchVisitor: ElementVisitor {
public var foundElement: Element? // Stores the first element that matches criteria
public var allFoundElements: [Element] = [] // Stores all elements that match criteria
private let criteria: [Criterion]
internal let stopAtFirstMatchInternal: Bool
private let maxDepth: Int
private var currentMaxDepthReachedByVisitor: Int = 0
private let matchType: JSONPathHintComponent.MatchType // Added
private let matchAllCriteriaBool: Bool // Added (renamed to avoid conflict with func name)
init(
criteria: [Criterion],
matchType: JSONPathHintComponent.MatchType = .exact, // Added with default
matchAllCriteria: Bool = true, // Added with default
stopAtFirstMatch: Bool = false,
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) {
self.criteria = criteria
self.matchType = matchType // Store
self.matchAllCriteriaBool = matchAllCriteria // Store
self.stopAtFirstMatchInternal = stopAtFirstMatch
self.maxDepth = maxDepth
logger.debug("SearchVisitor Init: Criteria: \(criteria.map { "\($0.attribute):\($0.value)(\($0.match_type?.rawValue ?? "exact"))" }.joined(separator: ", ")), StopAtFirst: \(stopAtFirstMatchInternal), MaxDepth: \(maxDepth), MatchType: \(matchType), MatchAll: \(matchAllCriteria)")
}
@MainActor
public func visit(element: Element, depth: Int) -> TreeVisitorResult {
currentMaxDepthReachedByVisitor = max(currentMaxDepthReachedByVisitor, depth)
if depth > maxDepth {
logger.debug("SearchVisitor: Max depth \(maxDepth) reached internally at [\(element.briefDescription(option: ValueFormatOption.smart))]. Skipping.")
return .skipChildren // Or .stop, depending on desired behavior beyond maxDepth by visitor
}
let elementDesc = element.briefDescription(option: ValueFormatOption.smart)
logger.debug("SearchVisitor Visiting: [\(elementDesc)] at depth \(depth). Criteria: \(criteria.map { "\($0.attribute):\($0.value)"}.joined(separator: ", "))")
var matches = false
if matchAllCriteriaBool {
// Use the stored matchType
if elementMatchesAllCriteria(element: element, criteria: criteria, matchType: self.matchType) {
matches = true
}
} else {
// Use the stored matchType
if elementMatchesAnyCriterion(element: element, criteria: criteria, matchType: self.matchType) {
matches = true
}
}
if matches {
logger.debug("SearchVisitor MATCH: [\(elementDesc)] at depth \(depth).")
foundElement = element
allFoundElements.append(element)
if stopAtFirstMatchInternal {
logger.debug("SearchVisitor: stopAtFirstMatchInternal is true. Stopping search.")
return .stop
}
} else {
logger.debug("SearchVisitor NO MATCH: [\(elementDesc)] at depth \(depth).")
}
return .continue
}
// Resets the visitor state for reuse, e.g., when searching different branches of a tree.
public func reset() {
self.foundElement = nil
self.allFoundElements.removeAll()
self.currentMaxDepthReachedByVisitor = 0 // Reset depth
// logger.debug("SearchVisitor reset.") // Optional: for debugging visitor lifecycle
}
}
// MARK: - Collect All Visitor Implementation
@MainActor
/* public -> internal */ internal func collectAll(
appElement: Element,
locator: Locator,
currentElement: Element,
depth: Int,
maxDepth: Int,
maxElements: Int?,
visitor: CollectAllVisitor
) async -> [AXElementData]? {
var traverser = TreeTraverser()
var state = TraversalState(maxDepth: maxDepth, startElement: currentElement)
_ = await traverser.traverse(from: currentElement, visitor: visitor, state: &state)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "collectAll: Traversal complete. Collected \\(visitor.collectedElements.count) elements."))
return visitor.collectedElements
public class CollectAllVisitor: ElementVisitor {
private(set) var collectedElements: [Element] = []
let criteria: [Criterion]?
let includeIgnored: Bool
init(criteria: [Criterion]? = nil, includeIgnored: Bool = false) {
self.criteria = criteria
self.includeIgnored = includeIgnored
let criteriaDebug = criteria?.map { "\($0.attribute):\($0.value)(\($0.match_type?.rawValue ?? "exact"))" }.joined(separator: ", ") ?? "all"
logger.debug("CollectAllVisitor Init: Criteria: [\(criteriaDebug)], IncludeIgnored: \(includeIgnored)")
}
public func visit(element: Element, depth: Int) -> TreeVisitorResult {
let elementDesc = element.briefDescription(option: ValueFormatOption.smart)
logger.debug("CollectAllVisitor Visiting: [\(elementDesc)] at depth \(depth).")
if !includeIgnored && element.isIgnored() {
logger.debug("CollectAllVisitor: Skipping ignored element [\(elementDesc)] because includeIgnored is false.")
return .skipChildren // Skip ignored elements and their children if not including ignored
}
if let criteria = criteria {
if elementMatchesAllCriteria(element: element, criteria: criteria) {
logger.debug("CollectAllVisitor: Adding [\(elementDesc)] (matched criteria).")
collectedElements.append(element)
} else {
logger.debug("CollectAllVisitor: [\(elementDesc)] did NOT match criteria.")
}
} else {
// No criteria, collect all (respecting includeIgnored)
logger.debug("CollectAllVisitor: Adding [\(elementDesc)] (no criteria given).")
collectedElements.append(element)
}
return .continue
}
}
// Remaining functions in this file (like path navigation helpers if any outside findElementViaPathAndCriteria)
// would need similar review and refactoring if they use the old logging pattern.
// MARK: - Element Search Logic
// [REMOVED OLD findElement FUNCTION]
// MARK: - Path Navigator (Remains mostly the same, but uses TraversalContext for logging)
// [REMOVED OLD navigateToElementByPath FUNCTION]
// MARK: - Tree Traversal Utilities (SearchVisitor, TreeTraverser, TraversalState)
// SearchVisitor is now defined in TreeTraversal.swift to avoid duplication
// Note: Ensure `getApplicationElement` from PathNavigator is accessible and synchronous.
// Ensure `navigateToElementByJSONPathHint` from PathNavigator is accessible and synchronous.
// Ensure `elementMatchesAllCriteria` from SearchCriteriaUtils is accessible and synchronous.
// Ensure `Criterion` struct and `Locator` struct are defined and accessible.
// AXMiscConstants should be available. Example: public enum AXMiscConstants { public static let defaultMaxDepthSearch: Int = 10 }

View File

@ -3,8 +3,12 @@
import ApplicationServices
import Foundation
import AppKit // Added for NSRunningApplication
import Logging // Import Logging
// Note: Assumes Element, PathUtils, Attribute are available.
// Note: Assumes Element, PathUtils, Attribute, AXMiscConstants are available.
// Define logger for this file
private let logger = Logger(label: "AXorcist.PathNavigator")
// New helper to check if an element matches all given criteria
@MainActor
@ -12,28 +16,33 @@ private func elementMatchesAllCriteria(
_ element: Element,
criteria: [String: String],
forPathComponent pathComponentForLog: String // For logging
) async -> Bool {
let elementDescriptionForLog = element.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Checking element [\\(elementDescriptionForLog)] against criteria for component [\\(pathComponentForLog)]. Criteria count: \\(criteria.count). Criteria: \\(criteria)"))
) -> Bool {
let elementDescriptionForLog = element.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_START: Checking element [\(elementDescriptionForLog)] for component [\(pathComponentForLog)]. Criteria: \(criteria)"))
if criteria.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Criteria empty for component [\\(pathComponentForLog)]. Element [\\(elementDescriptionForLog)] considered a match by default."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Criteria empty for component [\(pathComponentForLog)]. Element [\(elementDescriptionForLog)] considered a match by default."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED (empty criteria) for component [\(pathComponentForLog)]."))
return true
}
for (key, expectedValue) in criteria {
// Determine matchType based on key or default to .exact
// This is a simplified placeholder. Real logic might infer from key or have explicit matchTypes per criterion.
let matchTypeForKey: JSONPathHintComponent.MatchType = (key.lowercased() == AXAttributeNames.kAXDOMClassListAttribute.lowercased()) ? .contains : .exact
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC_CRITERION: Checking criterion '\(key): \(expectedValue)' (matchType: \(matchTypeForKey.rawValue)) on element [\(elementDescriptionForLog)] for component [\(pathComponentForLog)]."))
// matchSingleCriterion is async
if await !matchSingleCriterion(element: element, key: key, expectedValue: expectedValue, matchType: matchTypeForKey, elementDescriptionForLog: elementDescriptionForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\\(elementDescriptionForLog)] FAILED to match criterion '\\(key): \\(expectedValue)' for component [\\(pathComponentForLog)]."))
let criterionDidMatch = matchSingleCriterion(element: element, key: key, expectedValue: expectedValue, matchType: matchTypeForKey, elementDescriptionForLog: elementDescriptionForLog)
let message = "PathNav/EMAC_CRITERION_RESULT: Criterion '\(key): \(expectedValue)' on [\(elementDescriptionForLog)] for [\(pathComponentForLog)]: \(criterionDidMatch ? "MATCHED" : "FAILED")"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: message))
if !criterionDidMatch {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\(elementDescriptionForLog)] FAILED to match criterion '\(key): \(expectedValue)' for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] FAILED for component [\(pathComponentForLog)]."))
return false
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\\(elementDescriptionForLog)] successfully MATCHED ALL criteria for component [\\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\(elementDescriptionForLog)] successfully MATCHED ALL criteria for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED ALL criteria for component [\(pathComponentForLog)]."))
return true
}
@ -42,8 +51,8 @@ private func elementMatchesAllCriteria(
internal func navigateToElement(
from startElement: Element,
pathHint: [String],
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) async -> Element? {
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) -> Element? {
var currentElement = startElement
var currentPathSegmentForLog = ""
@ -56,17 +65,17 @@ internal func navigateToElement(
}
if index >= maxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation aborted: Path hint index \\(index) reached maxDepth \\(maxDepth). Path so far: \\(currentPathSegmentForLog)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation aborted: Path hint index \(index) reached maxDepth \(maxDepth). Path so far: \(currentPathSegmentForLog)"))
return nil
}
let criteriaToMatch = PathUtils.parseRichPathComponent(pathComponentString)
guard !criteriaToMatch.isEmpty else {
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty or unparsable criteria from pathComponentString '\\(pathComponentString)'"))
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty or unparsable criteria from pathComponentString '\(pathComponentString)'"))
return nil
}
if let nextElement = await processPathComponent(
if let nextElement = processPathComponent(
currentElement: currentElement,
pathComponentString: pathComponentString,
criteriaToMatch: criteriaToMatch,
@ -78,7 +87,7 @@ internal func navigateToElement(
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation successful. Final element: \\(currentElement.briefDescription(option: .smart))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation successful. Final element: \(currentElement.briefDescription(option: ValueFormatOption.smart))"))
return currentElement
}
@ -88,41 +97,41 @@ private func processPathComponent(
pathComponentString: String,
criteriaToMatch: [String: String],
currentPathSegmentForLog: String
) async -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC_DIRECT_LOG: Entered for \\(pathComponentString)"))
) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC_DIRECT_LOG: Entered for \(pathComponentString)"))
var stepCounter = 0
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). Before briefDesc."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). Before briefDesc."))
stepCounter += 1
let briefDesc = currentElement.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). Before logPathComponentProcessing. BriefDesc: \\(briefDesc)"))
let briefDesc = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). Before logPathComponentProcessing. BriefDesc: \(briefDesc)"))
stepCounter += 1
logPathComponentProcessing(pathComponentString: pathComponentString, briefDesc: briefDesc)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). After logPathComponentProcessing. Before PRE-CALL FMIC."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). After logPathComponentProcessing. Before PRE-CALL FMIC."))
stepCounter += 1
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: PRE-CALL FMIC"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). After PRE-CALL FMIC. Before findMatchingChild call."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). After PRE-CALL FMIC. Before findMatchingChild call."))
stepCounter += 1
if let matchedChild = await findMatchingChild(
if let matchedChild = findMatchingChild(
currentElement: currentElement,
criteriaToMatch: criteriaToMatch,
pathComponentForLog: pathComponentString
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). findMatchingChild returned non-nil."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). findMatchingChild returned non-nil."))
return matchedChild
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). findMatchingChild returned nil. Before elementMatchesAllCriteria."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). findMatchingChild returned nil. Before elementMatchesAllCriteria."))
stepCounter += 1
if await elementMatchesAllCriteria(currentElement, criteria: criteriaToMatch, forPathComponent: pathComponentString) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Current element \\(briefDesc) itself matches component '\\(pathComponentString)'. Retaining current element for this step."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). elementMatchesAllCriteria on currentElement was true."))
if elementMatchesAllCriteria(currentElement, criteria: criteriaToMatch, forPathComponent: pathComponentString) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Current element \(briefDesc) itself matches component '\(pathComponentString)'. Retaining current element for this step."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). elementMatchesAllCriteria on currentElement was true."))
return currentElement
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). elementMatchesAllCriteria on currentElement was false. Before logNoMatchFound."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). elementMatchesAllCriteria on currentElement was false. Before logNoMatchFound."))
stepCounter += 1
logNoMatchFound(
@ -130,13 +139,13 @@ private func processPathComponent(
pathComponentString: pathComponentString,
currentPathSegmentForLog: currentPathSegmentForLog
)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \\(stepCounter). After logNoMatchFound. Returning nil."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). After logNoMatchFound. Returning nil."))
return nil
}
@MainActor
private func logPathComponentProcessing(pathComponentString: String, briefDesc: String) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigating: Processing path component '\\(pathComponentString)' from current element: \\(briefDesc)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)"))
}
@MainActor
@ -145,7 +154,7 @@ private func logNoMatchFound(
pathComponentString: String,
currentPathSegmentForLog: String
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Neither current element \\(briefDesc) nor its children (after all checks) matched criteria for path component '\\(pathComponentString)'. Path: \\(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Neither current element \(briefDesc) nor its children (after all checks) matched criteria for path component '\(pathComponentString)'. Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER"))
}
@MainActor
@ -153,37 +162,52 @@ private func findMatchingChild(
currentElement: Element,
criteriaToMatch: [String: String],
pathComponentForLog: String
) async -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Entered. CurrentElement: \\(currentElement.briefDescription(option: .smart)). Component: \\(pathComponentForLog)"))
) -> Element? {
let parentElementDesc = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC_START: Searching children of [\(parentElementDesc)] for component [\(pathComponentForLog)]. Criteria: \(criteriaToMatch)"))
guard let children = currentElement.children() else {
let currentElementDescForLog = currentElement.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element [\\(currentElementDescForLog)] has no children (returned nil for .children())."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Element [\(parentElementDesc)] has no children (returned nil for .children())."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC_END: No children for [\(parentElementDesc)]. Returning nil."))
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Element \\(currentElement.briefDescription(option: .smart)) has \\(children.count) children. Iterating..."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Element \(parentElementDesc) has \(children.count) children. Iterating..."))
for child in children {
let childDesc = child.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Checking child [\\(childDesc)] against criteria for component [\\(pathComponentForLog)]."))
if await elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Child [\\(childDesc)] MATCHED for path component [\\(pathComponentForLog)]."))
if children.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Element \(parentElementDesc) has an empty children array."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC_END: Empty children array for [\(parentElementDesc)]. Returning nil."))
return nil
}
for (childIndex, child) in children.enumerated() {
let childDesc = child.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC_CHILD: [Child \(childIndex + 1)/\(children.count)] Processing child [\(childDesc)] of [\(parentElementDesc)] for component [\(pathComponentForLog)]."))
let childMatched = elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog)
let message = "PathNav/FMC_CHILD_RESULT: Child [\(childDesc)] of [\(parentElementDesc)] for [\(pathComponentForLog)]: \(childMatched ? "MATCHED" : "DID NOT MATCH")"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: message))
if childMatched {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Child [\(childDesc)] MATCHED for path component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC_END: Found matching child [\(childDesc)] for [\(parentElementDesc)]. Returning child."))
return child
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Child [\\(childDesc)] did NOT match criteria for [\\(pathComponentForLog)]. Continuing."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Child [\(childDesc)] did NOT match criteria for [\(pathComponentForLog)]. Continuing."))
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: No child of \\(currentElement.briefDescription(option: .smart)) matched criteria for [\\(pathComponentForLog)]. Returning nil."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: No child of \(parentElementDesc) matched criteria for [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC_END: No matching child found for [\(parentElementDesc)]. Returning nil."))
return nil
}
@MainActor
private func getChildrenFromElement(_ element: Element) async -> [Element]? {
private func getChildrenFromElement(_ element: Element) -> [Element]? {
guard let children = element.children() else {
let currentElementDescForLog = element.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element [\\(currentElementDescForLog)] has no children (returned nil for .children())."))
let currentElementDescForLog = element.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element [\(currentElementDescForLog)] has no children (returned nil for .children())."))
return nil
}
if children.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element [\\(element.briefDescription(option: .smart))] has zero children (returned empty array for .children())."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element [\(element.briefDescription(option: ValueFormatOption.smart))] has zero children (returned empty array for .children())."))
}
return children
}
@ -198,13 +222,13 @@ private func getChildrenFromElement(_ element: Element) async -> [Element]? {
// (e.g., by returning a special key like "@index" in criteriaToMatch)
// In processPathComponent, after trying findMatchingChild and elementMatchesAllCriteria(currentElement...):
if let indexStr = criteriaToMatch["@index"], let index = Int(indexStr) {
if let children = getChildrenFromElement(currentElement), index >= 0, index < children.count {
if let indexStr = criteriaToMatch[\"@index\"], let index = Int(indexStr) {
if let children = await getChildrenFromElement(currentElement), index >= 0, index < children.count { // Added await
let indexedChild = children[index]
axDebugLog("Path component '\(pathComponentString)' resolved to child at index \(index): \(indexedChild.briefDescription())")
await axDebugLog(\"Path component \'\\(pathComponentString)\' resolved to child at index \\(index): \\(await indexedChild.briefDescription())\") // Added await
return indexedChild
} else {
axDebugLog("Path component '\(pathComponentString)' (index \(index)) out of bounds for \(currentElement.briefDescription()) with \(getChildrenFromElement(currentElement)?.count ?? 0) children.")
await axDebugLog(\"Path component \'\\(pathComponentString)\' (index \\(index)) out of bounds for \\(await currentElement.briefDescription()) with \\(await getChildrenFromElement(currentElement)?.count ?? 0) children.\") // Added await
// logNoMatchFound would have been called if attribute matching failed before this.
// If ONLY index was provided and it failed, this is the failure point.
return nil
@ -224,277 +248,263 @@ internal func original_currentElementMatchesPathComponent( // Marked as original
_ element: Element,
attributeName: String,
expectedValue: String
) -> Bool {
) async -> Bool { // Made async
if attributeName.isEmpty {
axWarningLog("original_currentElementMatchesPathComponent: attributeName is empty.")
await axWarningLog(\"original_currentElementMatchesPathComponent: attributeName is empty.\") // Added await
return false
}
if let actualValue: String = element.attribute(Attribute(attributeName)) {
if actualValue == expectedValue {
return true
// ... (rest of original function would need similar async/await updates for attribute access and logging) ...
}
*/
// MARK: - JSON PathHint Navigation
// Helper to convert JSONPathHintComponent.AttributeName to actual AXAttribute string
// This might be better placed in a utility struct/enum for AttributeName if it becomes complex
// For now, a simple switch based on the rawValue of the enum.
// UPDATE: This function is problematic because JSONPathHintComponent.AttributeName does not exist.
// The `attribute` in JSONPathHintComponent is already a String.
// This function might have been intended for an earlier version of JSONPathHintComponent.
// Keeping it commented out for now. If direct attribute string usage in JSONPathHintComponent is correct, this is not needed.
/*
private func jsonPathHintAttrToAXAttribute(_ attrName: JSONPathHintComponent.AttributeName) -> String {
switch attrName {
case .role: return AXAttributeNames.kAXRoleAttribute
case .subrole: return AXAttributeNames.kAXSubroleAttribute
case .identifier: return AXAttributeNames.kAXIdentifierAttribute
case .title: return AXAttributeNames.kAXTitleAttribute
case .value: return AXAttributeNames.kAXValueAttribute
case .description: return AXAttributeNames.kAXDescriptionAttribute
// Add other cases as necessary from JSONPathHintComponent.AttributeName
default:
// Fallback or error for unhandled cases
// For now, using the rawValue, but this implies AttributeName has a rawValue or is a string itself.
// This needs to be aligned with the actual definition of JSONPathHintComponent.AttributeName.
// If attrName is already the string (e.g. "AXRole"), then this function is not needed.
// The error "String has no member rawValue" likely points to this.
// If JSONPathHintComponent.attribute is already a String, this function becomes:
// private func jsonPathHintAttrToAXAttribute(_ attrName: String) -> String { return attrName }
// ... or it's just used directly.
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "jsonPathHintAttrToAXAttribute: Unhandled or direct-use attribute name '\(attrName)'. Using rawValue if available, otherwise direct string."))
// Assuming attrName might conform to RawRepresentable<String> if it's an enum
// Or if it's already a string, this part is overly complex.
if let raw = (attrName as? any RawRepresentable)?.rawValue as? String {
return raw
}
return String(describing: attrName) // Fallback, likely incorrect if attrName isn't directly the string.
}
}
*/
// Updated navigateToElementByJSONPathHint to use the new Element API and logging
@MainActor
internal func navigateToElementByJSONPathHint(
from startElement: Element,
jsonPathHint: [JSONPathHintComponent],
overallMaxDepth: Int = AXMiscConstants.defaultMaxDepthSearch,
initialPathSegmentForLog: String = "Root"
) -> Element? {
var currentElement = startElement
var currentPathSegmentForLog = initialPathSegmentForLog
let pathHintCount = jsonPathHint.count
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV_START: From [\(startElement.briefDescription(option: ValueFormatOption.smart))] with hint (count: \(pathHintCount)): \(jsonPathHint.map { $0.descriptionForLog() }.joined(separator: " -> "))"))
for (index, pathComponent) in jsonPathHint.enumerated() {
let componentDescForLog = pathComponent.descriptionForLog()
currentPathSegmentForLog += (index > 0 ? " -> " : " (Start) -> ") + componentDescForLog
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV_COMPONENT [\(index + 1)/\(pathHintCount)]: Processing '\(componentDescForLog)'. Current path: [\(currentPathSegmentForLog)]"))
let depthForThisStep = pathComponent.depth ?? AXMiscConstants.defaultMaxDepthSearchForHintStep
if index >= overallMaxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV: Path hint index \(index) reached overallMaxDepth \(overallMaxDepth). Path so far: \(currentPathSegmentForLog)"))
return nil
}
let attributeToMatch = pathComponent.attribute
let valueToMatch = pathComponent.value
let matchType = pathComponent.matchType ?? .exact
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV_COMPONENT_DETAILS: Attribute: '\(attributeToMatch)', Value: '\(valueToMatch)', MatchType: '\(matchType.rawValue)', DepthForStep: \(depthForThisStep)"))
let searchCriteria = [Criterion(attribute: attributeToMatch, value: valueToMatch, matchType: matchType)]
let foundElement = findDescendantMatchingCriteria(
startElement: currentElement,
criteria: searchCriteria,
maxDepth: depthForThisStep,
stopAtFirstMatch: true,
pathComponentForLog: componentDescForLog
)
if let nextElement = foundElement {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV_MATCH: Component '\(componentDescForLog)' matched by [\(nextElement.briefDescription(option: ValueFormatOption.smart))]. Updating current element."))
currentElement = nextElement
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON_NAV_NO_MATCH: Component '\(componentDescForLog)' did not match any element from [\(currentElement.briefDescription(option: ValueFormatOption.smart))] within depth \(depthForThisStep). Path: \(currentPathSegmentForLog)"))
return nil
}
}
return false
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JSON_NAV_SUCCESS: Navigation successful. Final element: [\(currentElement.briefDescription(option: ValueFormatOption.smart))] after path: [\(currentPathSegmentForLog)]"))
return currentElement
}
@MainActor
private func original_checkChildMatch( // Marked as original
child: Element,
attributeName: String,
expectedValue: String
private func findDescendantMatchingCriteria(
startElement: Element,
criteria: [Criterion],
maxDepth: Int,
stopAtFirstMatch: Bool,
pathComponentForLog: String
) -> Element? {
let childBriefDescForLog = child.briefDescription(option: .smart)
guard let actualValue: String = child.attribute(Attribute(attributeName)) else {
if elementMatchesAllCriteria(element: startElement, criteria: criteria) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FDMC: Start element [\(startElement.briefDescription(option: ValueFormatOption.smart))] itself matches criteria for path component '\(pathComponentForLog)'."))
return startElement
}
if maxDepth <= 0 {
return nil
}
original_logChildCheck( // Use original log
childDesc: childBriefDescForLog,
attributeName: attributeName,
actualValue: actualValue,
expectedValue: expectedValue
)
guard let children = startElement.children() else {
return nil
}
if actualValue == expectedValue {
original_logChildMatch( // Use original log
childDesc: childBriefDescForLog,
attributeName: attributeName,
expectedValue: expectedValue
)
return child
for child in children {
if let found = findDescendantMatchingCriteria(
startElement: child,
criteria: criteria,
maxDepth: maxDepth - 1,
stopAtFirstMatch: stopAtFirstMatch,
pathComponentForLog: pathComponentForLog
) {
if stopAtFirstMatch {
return found
}
}
}
return nil
}
@MainActor
private func original_logChildCheck( // Marked as original
childDesc: String,
attributeName: String,
actualValue: String,
expectedValue: String
) {
let matchStatus = (actualValue == expectedValue) ? "==" : "!="
axDebugLog(
"Checking child: \(childDesc) | Attribute: \(attributeName) | Actual: '\(actualValue)' \(matchStatus) Expected: '\(expectedValue)'",
file: #file,
function: #function,
line: #line
)
}
// MARK: - Application Root Element Navigation
@MainActor
private func original_logChildMatch( // Marked as original
childDesc: String,
attributeName: String,
expectedValue: String
) {
axDebugLog(
"MATCHED child: \(childDesc) for \(attributeName):\(expectedValue)",
file: #file,
function: #function,
line: #line
)
}
*/
// MARK: - JSON Path Hint Navigation
// Main external entry point for JSON path hint navigation
@MainActor
func navigateToElementByJSONPathHint(
from startElement: Element,
pathHintComponents: [JSONPathHintComponent]
) async -> Element? {
var currentElement = startElement
let pathDescriptionForLog = pathHintComponents.map { "\($0.attribute):\($0.value)" }.joined(separator: " -> ")
let initialMessage = "PathNav/JSON: Starting navigation with \\(pathHintComponents.count) JSON components from \\(currentElement.briefDescription(option: .smart)). Path: \\(pathDescriptionForLog)"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: initialMessage))
for (index, component) in pathHintComponents.enumerated() {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON: Processing component \\(index + 1)/\\(pathHintComponents.count): [\\(component.attribute):\\(component.value)] from current element [\\(currentElement.briefDescription(option: .smart))]"))
if let nextElement = await findDescendantMatchingCriteria(
startingFrom: currentElement,
hintComponent: component,
pathComponentForLog: "JSONHintStep_\\(index)_\(component.attribute)"
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JSON: Component \\(index + 1) matched. New current element: [\\(nextElement.briefDescription(option: .smart))]"))
currentElement = nextElement
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/JSON: Component \\(index + 1) [\\(component.attribute):\\(component.value)] did NOT match any descendant from [\\(currentElement.briefDescription(option: .smart))]. Navigation failed."))
return nil
}
public func getApplicationElement(for bundleIdentifier: String) -> Element? {
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/AppEl: No running application found for bundle ID '\(bundleIdentifier)'."))
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JSON: Navigation successful. Final element: [\\(currentElement.briefDescription(option: .smart))]"))
return currentElement
let pid = runningApp.processIdentifier
let appElement = Element(AXUIElementCreateApplication(pid))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for '\(bundleIdentifier)' (PID: \(pid)): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
return appElement
}
// Searches descendants of `startingFrom` (inclusive of self if depth allows) for an element matching criteria in `hintComponent`.
@MainActor
private func findDescendantMatchingCriteria(
startingFrom element: Element,
hintComponent: JSONPathHintComponent,
pathComponentForLog: String // For logging, e.g., "JSONHintStep_0_ROLE"
) async -> Element? {
let currentElementDesc = element.briefDescription(option: .smart)
let matchTypeRawValue = hintComponent.matchType?.rawValue ?? "exact_fallback"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FindDesc: Searching for [\\(hintComponent.attribute):\\(hintComponent.value)] starting from [\\(currentElementDesc)] with depth \\(hintComponent.depth ?? -1)"))
// Convert JSONPathHintComponent to a [String: String] criteria map
// This uses the mapped AXAttributeName (e.g., kAXRoleAttribute)
var criteria: [String: String] = [:]
if let axAttr = hintComponent.axAttributeName {
criteria[axAttr] = hintComponent.value
public func getApplicationElement(for processId: pid_t) -> Element? {
let appElement = Element(AXUIElementCreateApplication(processId))
let bundleIdMessagePart: String
if let runningApp = NSRunningApplication(processIdentifier: processId), let bId = runningApp.bundleIdentifier {
bundleIdMessagePart = " (\(bId))"
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "PathNav/FindDesc: Unknown attribute type '\\(hintComponent.attribute)' in JSON hint. Cannot build criteria."))
return nil
bundleIdMessagePart = ""
}
let maxDepthToSearch = hintComponent.depth ?? JSONPathHintComponent.defaultDepthForSegment // Use component depth or default step depth
// Use a breadth-first or depth-first search up to maxDepthToSearch
// For simplicity, using a recursive depth-limited search helper.
// This helper will check the current element first, then its children, respecting depth.
return await searchRecursiveForCriteria(
currentElement: element,
criteria: criteria,
matchType: hintComponent.matchType,
currentDepth: 0,
maxDepth: maxDepthToSearch,
pathComponentForLog: pathComponentForLog
)
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for PID \(processId)\(bundleIdMessagePart): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
return appElement
}
// Recursive helper for findDescendantMatchingCriteria
// MARK: - Element from Path (High-Level)
@MainActor
private func searchRecursiveForCriteria(
currentElement: Element,
criteria: [String: String],
matchType: JSONPathHintComponent.MatchType?,
currentDepth: Int,
public func getElement(
appIdentifier: String,
pathHint: [Any],
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Attempting to get element for app '\(appIdentifier)' with path hint (count: \(pathHint.count))."))
let startElement: Element?
if let pid = pid_t(appIdentifier) {
startElement = getApplicationElement(for: pid)
} else {
startElement = getApplicationElement(for: appIdentifier)
}
guard let rootElement = startElement else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/GetEl: Could not get root application element for '\(appIdentifier)'."))
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Root element for '\(appIdentifier)' is [\(rootElement.briefDescription(option: ValueFormatOption.smart))]. Processing path hint."))
if let stringPathHint = pathHint as? [String] {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Interpreting path hint as [String]. Count: \(stringPathHint.count). Hint: \(stringPathHint.joined(separator: " -> "))"))
return navigateToElement(from: rootElement, pathHint: stringPathHint, maxDepth: maxDepth)
} else if let jsonPathHint = pathHint as? [JSONPathHintComponent] {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Interpreting path hint as [JSONPathHintComponent]. Count: \(jsonPathHint.count). Hint: \(jsonPathHint.map { $0.descriptionForLog() }.joined(separator: " -> "))"))
let initialLogSegment = rootElement.role() == AXRoleNames.kAXApplicationRole ? "Application" : rootElement.briefDescription(option: ValueFormatOption.smart)
return navigateToElementByJSONPathHint(from: rootElement, jsonPathHint: jsonPathHint, overallMaxDepth: maxDepth, initialPathSegmentForLog: initialLogSegment)
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "PathNav/GetEl: Path hint type is not [String] or [JSONPathHintComponent]. Hint: \(pathHint). Cannot navigate."))
return nil
}
}
@MainActor
func findDescendantAtPath(
currentRoot: Element,
pathComponents: [PathStep],
maxDepth: Int,
pathComponentForLog: String
) async -> Element? {
let currentElementDesc = currentElement.briefDescription(option: .smart)
let matchTypeRawValue = matchType?.rawValue ?? "exact_fallback"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: Visiting [\\(currentElementDesc)] at depth \\(currentDepth) (max: \\(maxDepth)) for criteria [\\(criteria)] (match: \\(matchTypeRawValue))"))
debugSearch: Bool
) -> Element? {
var currentElement = currentRoot
logger.debug("PathNav/findDescendantAtPath: Starting path navigation. Initial root: \\(currentElement.briefDescription(option: .smart)). Path components: \\(pathComponents.count)")
// Check if current element matches
// elementMatchesAllCriteria is now synchronous
if await elementMatchesAllCriteria(currentElement, criteria: criteria, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: [\\(currentElementDesc)] MATCHED criteria at depth \\(currentDepth). PathComponent: \\(pathComponentForLog)"))
return currentElement
}
for (_, component) in pathComponents.enumerated() {
// Log messages will use pathComponents.count if needed, index isn't critical for current logging
logger.debug("PathNav/findDescendantAtPath: Processing component. Current: \\(currentElement.briefDescription(option: .smart))")
let searchVisitor = SearchVisitor(
criteria: component.criteria,
matchType: component.matchType ?? .exact,
matchAllCriteria: component.matchAllCriteria ?? true,
stopAtFirstMatch: true,
maxDepth: component.maxDepthForStep ?? 1
)
// If maxDepth reached or no children, stop descent
if currentDepth >= maxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: Max depth \\(maxDepth) reached for [\\(currentElementDesc)]. PathComponent: \\(pathComponentForLog). No deeper search."))
return nil
}
// Element.children() is now synchronous
guard let children = currentElement.children(), !children.isEmpty else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: [\\(currentElementDesc)] has no children or children array is empty. PathComponent: \\(pathComponentForLog). No deeper search."))
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: [\\(currentElementDesc)] has \\(children.count) children. Iterating..."))
for child in children {
// searchRecursiveForCriteria is now synchronous
if let matchedElement = await searchRecursiveForCriteria(
currentElement: child,
criteria: criteria,
matchType: matchType,
currentDepth: currentDepth + 1,
maxDepth: maxDepth,
pathComponentForLog: pathComponentForLog
) {
return matchedElement // Found in a descendant
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/SearchRec: No match found in [\\(currentElementDesc)] or its descendants up to depth \\(maxDepth) for criteria. PathComponent: \\(pathComponentForLog)."))
return nil // Not found in this branch
}
// Determines the starting element for a search based on path hints.
@MainActor
func processJSONPathHintAndDetermineStartElement(
for appBundleID: String?,
windowTitleHint: String?,
pathHint: [JSONPathHintComponent]?
) async -> Element? {
let logMessage = "PathNav/ProcJSONHint: app=\(appBundleID ?? "nil"), window=\(windowTitleHint ?? "nil"), hintCount=\(pathHint?.count ?? 0)"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: logMessage))
var startSearchElement: Element? = nil
if let bundleID = appBundleID, !bundleID.isEmpty {
// Element.application(bundleIdentifier:) is now synchronous if we recreate it or use a sync alternative
// For now, assuming a synchronous way to get the app element:
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).first else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/ProcJSONHint: No running application found for bundle ID '\\(bundleID)'."))
// Children of the current element are where we search for the next path component
logger.debug("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] Current element for child search: \\(currentElement.briefDescription(option: .smart))")
guard let childrenToSearch = currentElement.children(strict: false), !childrenToSearch.isEmpty else {
logger.warning("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] No children found (or list was empty) for \\(currentElement.briefDescription(option: .smart)). Path navigation cannot proceed further down this branch.")
return nil
}
let appElement = Element(AXUIElementCreateApplication(runningApp.processIdentifier))
// Basic check if appElement is valid (e.g., by trying to get its role)
if appElement.role() == nil { // role() is sync
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/ProcJSONHint: Failed to create a valid application Element for PID \\(runningApp.processIdentifier) from bundleID '\\(bundleID)'. Role check failed."))
return nil
}
startSearchElement = appElement
let appDesc = startSearchElement?.briefDescription(option: .smart) ?? "nil"
let appDescMessage = "PathNav/ProcJSONHint: Set start element to application [\\(bundleID)] - [\\(appDesc)]"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: appDescMessage))
logger.debug("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] Found \\(childrenToSearch.count) children to search.")
if let titleHint = windowTitleHint, !titleHint.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/ProcJSONHint: Window title hint '\\(titleHint)' provided. Searching for window in [\\(bundleID)]."))
// Search for the window within the application element
// Element.windows() and Element.title() are now synchronous
if let windows = startSearchElement?.windows() {
var foundWindow: Element? = nil
for window in windows {
if let windowTitle = window.title(), windowTitle.contains(titleHint) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/ProcJSONHint: Found matching window by title '\\(windowTitle)' (hint: '\\(titleHint)'))"))
foundWindow = window
break
}
}
if let window = foundWindow {
startSearchElement = window
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/ProcJSONHint: Updated start element to specific window [\\(window.briefDescription(option: .smart))]"))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/ProcJSONHint: Window with title containing '\\(titleHint)' not found in application [\\(bundleID)]. "))
return nil // Window hint provided but not found
}
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/ProcJSONHint: Application [\\(bundleID)] has no windows or failed to retrieve them."))
return nil // App has no windows
var foundMatchForThisComponent: Element? = nil
for child in childrenToSearch {
searchVisitor.reset()
traverseAndSearch(element: child, visitor: searchVisitor, currentDepth: 0, maxDepth: component.maxDepthForStep ?? 1)
if let foundUnwrapped = searchVisitor.foundElement {
logger.info("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] MATCHED component criteria \\(component.descriptionForLog()) on child: \\(foundUnwrapped.briefDescription(option: ValueFormatOption.smart))")
foundMatchForThisComponent = foundUnwrapped
break
}
}
} else {
// No application specified, use system-wide element
startSearchElement = Element.systemWide() // systemWide() is sync
let systemWideDesc = startSearchElement?.briefDescription(option: .smart) ?? "nil"
let systemWideMessage = "PathNav/ProcJSONHint: No app bundle ID. Defaulting start element to system-wide [\\(systemWideDesc)]"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: systemWideMessage))
}
guard let nonNilStartElement = startSearchElement else {
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "PathNav/ProcJSONHint: Could not determine a valid start search element."))
return nil
}
// If there's a path hint, navigate from the determined start element
if let hintComponents = pathHint, !hintComponents.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/ProcJSONHint: Path hint provided (\\(hintComponents.count) components). Navigating from [\\(nonNilStartElement.briefDescription(option: .smart))]"))
// navigateToElementByJSONPathHint is now synchronous
return await navigateToElementByJSONPathHint(from: nonNilStartElement, pathHintComponents: hintComponents)
} else {
// No path hint, so the start element (app or window or systemWide) is the target
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/ProcJSONHint: No path hint. Returning determined start element: [\\(nonNilStartElement.briefDescription(option: .smart))]"))
return nonNilStartElement
if let nextElement = foundMatchForThisComponent {
currentElement = nextElement
logger.debug("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] Advancing to next element: \\(currentElement.briefDescription(option: .smart))")
} else {
logger.warning("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] FAILED to find match for component criteria: \\(component.descriptionForLog()) within children of \\(currentElement.briefDescription(option: .smart))")
return nil
}
}
logger.info("PathNav/findDescendantAtPath: Successfully navigated full path. Final element: \\(currentElement.briefDescription(option: .smart))")
return currentElement
}

View File

@ -43,11 +43,11 @@ public struct PathHintComponent {
}
if mappedCriteria.isEmpty {
axWarningLog("PathHintComponent: Path segment '\\(pathSegment)' produced no usable criteria after parsing.")
axWarningLog("PathHintComponent: Path segment '\(pathSegment)' produced no usable criteria after parsing.")
return nil
}
self.criteria = mappedCriteria
axDebugLog("PathHintComponent initialized. Segment: '\\(pathSegment)' => criteria: \\(mappedCriteria)")
axDebugLog("PathHintComponent initialized. Segment: '\(pathSegment)' => criteria: \(mappedCriteria)")
}
init(criteria: [String: String], originalSegment: String = "") {
@ -56,45 +56,61 @@ public struct PathHintComponent {
}
// PathHintComponent uses exact matching by default when calling elementMatchesCriteria
func matches(element: Element) async -> Bool {
return await elementMatchesCriteria(element, criteria: self.criteria, matchType: .exact)
func matches(element: Element) -> Bool {
return elementMatchesCriteria(element, criteria: self.criteria, matchType: JSONPathHintComponent.MatchType.exact)
}
}
// MARK: - Criteria Matching Helper
@MainActor
public func elementMatchesCriteria(
_ element: Element,
public func elementMatchesAllCriteria(
element: Element,
criteria: [Criterion],
matchType: JSONPathHintComponent.MatchType = .exact
) async -> Bool {
if criteria.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesCriteria: Criteria dictionary is empty. Element '\\(element.briefDescription(option: .raw))' is considered a match by default."))
return true
}
) -> Bool {
for criterion in criteria {
let effectiveMatchType = criterion.match_type.flatMap { JSONPathHintComponent.MatchType(rawValue: $0) } ?? matchType
if await !matchSingleCriterion(element: element, key: criterion.attribute, expectedValue: criterion.value, matchType: effectiveMatchType, elementDescriptionForLog: element.briefDescription(option: .raw)) {
let effectiveMatchType = criterion.match_type ?? matchType
if !matchSingleCriterion(element: element, key: criterion.attribute, expectedValue: criterion.value, matchType: effectiveMatchType, elementDescriptionForLog: element.briefDescription(option: ValueFormatOption.raw)) {
return false
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesCriteria: Element '\\(element.briefDescription(option: .raw))' MATCHED ALL \\(criteria.count) criteria: \\(criteria)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesAllCriteria: Element '\(element.briefDescription(option: ValueFormatOption.raw))' MATCHED ALL \(criteria.count) criteria: \(criteria)."))
return true
}
@MainActor
public func elementMatchesAnyCriterion(
element: Element,
criteria: [Criterion],
matchType: JSONPathHintComponent.MatchType = .exact
) -> Bool {
if criteria.isEmpty { // If there are no criteria, it's vacuously false that any criterion matches.
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesAnyCriterion: No criteria provided. Returning false."))
return false
}
for criterion in criteria {
let effectiveMatchType = criterion.match_type ?? matchType // Use criterion's own match_type if present, else the overall one.
if matchSingleCriterion(element: element, key: criterion.attribute, expectedValue: criterion.value, matchType: effectiveMatchType, elementDescriptionForLog: element.briefDescription(option: ValueFormatOption.raw)) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesAnyCriterion: Element '\(element.briefDescription(option: ValueFormatOption.raw))' MATCHED criterion: \(criterion)."))
return true // Found one criterion that matches
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "elementMatchesAnyCriterion: Element '\(element.briefDescription(option: ValueFormatOption.raw))' DID NOT MATCH ANY of \(criteria.count) criteria: \(criteria)."))
return false
}
// Overload for backward compatibility with dictionary
@MainActor
public func elementMatchesCriteria(
_ element: Element,
criteria: [String: String],
matchType: JSONPathHintComponent.MatchType = .exact
) async -> Bool {
) -> Bool {
let criterionArray = criteria.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
Criterion(attribute: key, value: value, matchType: nil)
}
return await elementMatchesCriteria(element, criteria: criterionArray, matchType: matchType)
return elementMatchesAllCriteria(element: element, criteria: criterionArray, matchType: matchType)
}
// MARK: - Single Criterion Matching Logic
@ -106,33 +122,56 @@ internal func matchSingleCriterion(
expectedValue: String,
matchType: JSONPathHintComponent.MatchType,
elementDescriptionForLog: String
) async -> Bool {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC: Matching key '\\(key)' (expected: '\\(expectedValue)', type: \\(matchType.rawValue)) on \\(elementDescriptionForLog)"))
) -> Bool {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC: Matching key '\(key)' (expected: '\(expectedValue)', type: \(matchType.rawValue)) on \(elementDescriptionForLog)"))
let comparisonResult: Bool
switch key.lowercased() {
case AXAttributeNames.kAXRoleAttribute.lowercased(), "role":
return compareStrings(element.role(), expectedValue, matchType, attributeName: AXAttributeNames.kAXRoleAttribute, elementDescriptionForLog: elementDescriptionForLog)
let actual = element.role()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Role: Actual='\(actual ?? "nil")'"))
if actual == AXRoleNames.kAXTextAreaRole {
let domClassList = element.attribute(Attribute<Any>(AXAttributeNames.kAXDOMClassListAttribute))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "SearchCrit/MSC/Role: ELEMENT IS AXTextArea. Its AXDOMClassList is: \(String(describing: domClassList))"))
}
comparisonResult = compareStrings(actual, expectedValue, matchType, caseSensitive: false, attributeName: AXAttributeNames.kAXRoleAttribute, elementDescriptionForLog: elementDescriptionForLog)
case AXAttributeNames.kAXSubroleAttribute.lowercased(), "subrole":
return compareStrings(element.subrole(), expectedValue, matchType, attributeName: AXAttributeNames.kAXSubroleAttribute, elementDescriptionForLog: elementDescriptionForLog)
let actual = element.subrole()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Subrole: Actual='\(actual ?? "nil")'"))
comparisonResult = compareStrings(actual, expectedValue, matchType, caseSensitive: false, attributeName: AXAttributeNames.kAXSubroleAttribute, elementDescriptionForLog: elementDescriptionForLog)
case AXAttributeNames.kAXIdentifierAttribute.lowercased(), "identifier", "id":
return compareStrings(element.identifier(), expectedValue, matchType, attributeName: AXAttributeNames.kAXIdentifierAttribute, elementDescriptionForLog: elementDescriptionForLog)
let actual = element.identifier()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/ID: Actual='\(actual ?? "nil")'"))
comparisonResult = compareStrings(actual, expectedValue, matchType, caseSensitive: true, attributeName: AXAttributeNames.kAXIdentifierAttribute, elementDescriptionForLog: elementDescriptionForLog)
case "pid":
return matchPidCriterion(element: element, expectedValue: expectedValue, elementDescriptionForLog: elementDescriptionForLog)
comparisonResult = matchPidCriterion(element: element, expectedValue: expectedValue, elementDescriptionForLog: elementDescriptionForLog)
case AXAttributeNames.kAXDOMClassListAttribute.lowercased(), "domclasslist", "classlist":
return await matchDomClassListCriterion(element: element, expectedValue: expectedValue, matchType: matchType, elementDescriptionForLog: elementDescriptionForLog)
let actualRaw = element.attribute(Attribute<Any>(AXAttributeNames.kAXDOMClassListAttribute))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/DOMClassList: ActualRaw='\(String(describing: actualRaw))'"))
comparisonResult = matchDomClassListCriterion(element: element, expectedValue: expectedValue, matchType: matchType, elementDescriptionForLog: elementDescriptionForLog)
case AXMiscConstants.isIgnoredAttributeKey.lowercased(), "isignored", "ignored":
return matchIsIgnoredCriterion(element: element, expectedValue: expectedValue, elementDescriptionForLog: elementDescriptionForLog)
comparisonResult = matchIsIgnoredCriterion(element: element, expectedValue: expectedValue, elementDescriptionForLog: elementDescriptionForLog)
case AXMiscConstants.computedNameAttributeKey.lowercased(), "computedname", "name":
return await matchComputedNameAttributes(element: element, expectedValue: expectedValue, matchType: matchType, attributeName: AXMiscConstants.computedNameAttributeKey, elementDescriptionForLog: elementDescriptionForLog)
comparisonResult = matchComputedNameAttributes(element: element, expectedValue: expectedValue, matchType: matchType, attributeName: AXMiscConstants.computedNameAttributeKey, elementDescriptionForLog: elementDescriptionForLog)
case "computednamewithvalue", "namewithvalue":
return await matchComputedNameAttributes(element: element, expectedValue: expectedValue, matchType: matchType, attributeName: "computedNameWithValue", elementDescriptionForLog: elementDescriptionForLog, includeValueInComputedName: true)
comparisonResult = matchComputedNameAttributes(element: element, expectedValue: expectedValue, matchType: matchType, attributeName: "computedNameWithValue", elementDescriptionForLog: elementDescriptionForLog, includeValueInComputedName: true)
default:
guard let actualValue: String = element.attribute(Attribute(key)) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC: Attribute '\\(key)' not found or nil on \\(elementDescriptionForLog). No match."))
guard let actualValueAny: Any = element.attribute(Attribute(key)) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Default: Attribute '\(key)' not found or nil on \(elementDescriptionForLog). No match."))
return false
}
return compareStrings(actualValue, expectedValue, matchType, attributeName: key, elementDescriptionForLog: elementDescriptionForLog)
let actualValueString: String
if let str = actualValueAny as? String {
actualValueString = str
} else {
actualValueString = "\(actualValueAny)"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Default: Attribute '\(key)' on \(elementDescriptionForLog) was not String (type: \(type(of: actualValueAny))), using string description: '\(actualValueString)' for comparison."))
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Default: Attribute '\(key)', Actual='\(actualValueString)'"))
comparisonResult = compareStrings(actualValueString, expectedValue, matchType, caseSensitive: true, attributeName: key, elementDescriptionForLog: elementDescriptionForLog)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC: Key '\(key)', Expected='\(expectedValue)', MatchType='\(matchType.rawValue)', Result=\(comparisonResult) on \(elementDescriptionForLog)."))
return comparisonResult
}
// MARK: - Specific Criterion Matchers
@ -142,27 +181,27 @@ private func matchPidCriterion(element: Element, expectedValue: String, elementD
let expectedPid = expectedValue
if element.role() == AXRoleNames.kAXApplicationRole {
guard let actualPid_t = element.pid() else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) (app role) failed to provide PID. No match."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) failed to provide PID. No match."))
return false
}
if String(actualPid_t) == expectedPid {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) (app role) PID \\(actualPid_t) MATCHES expected \\(expectedPid)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) PID \(actualPid_t) MATCHES expected \(expectedPid)."))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) (app role) PID \\(actualPid_t) MISMATCHES expected \\(expectedPid)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) PID \(actualPid_t) MISMATCHES expected \(expectedPid)."))
return false
}
}
guard let actualPid_t = element.pid() else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) failed to provide PID. No match."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) failed to provide PID. No match."))
return false
}
let actualPidString = String(actualPid_t)
if actualPidString == expectedPid {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) PID \\(actualPidString) MATCHES expected \\(expectedPid)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) PID \(actualPidString) MATCHES expected \(expectedPid)."))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \\(elementDescriptionForLog) PID \\(actualPidString) MISMATCHES expected \\(expectedPid)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/PID: \(elementDescriptionForLog) PID \(actualPidString) MISMATCHES expected \(expectedPid)."))
return false
}
}
@ -172,18 +211,18 @@ private func matchIsIgnoredCriterion(element: Element, expectedValue: String, el
let actualIsIgnored: Bool = element.isIgnored()
let expectedBool = (expectedValue.lowercased() == "true")
if actualIsIgnored == expectedBool {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/IsIgnored: \\(elementDescriptionForLog) actual ('\\(actualIsIgnored)\') MATCHES expected ('\\(expectedBool)\')."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') MATCHES expected ('\(expectedBool)')."))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/IsIgnored: \\(elementDescriptionForLog) actual ('\\(actualIsIgnored)\') MISMATCHES expected ('\\(expectedBool)\')."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') MISMATCHES expected ('\(expectedBool)')."))
return false
}
}
@MainActor
private func matchDomClassListCriterion(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, elementDescriptionForLog: String) async -> Bool {
private func matchDomClassListCriterion(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, elementDescriptionForLog: String) -> Bool {
guard let domClassListValue: Any = element.attribute(Attribute(AXAttributeNames.kAXDOMClassListAttribute)) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \\(elementDescriptionForLog) attribute was nil. No match."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) attribute was nil. No match."))
return false
}
@ -194,44 +233,64 @@ private func matchDomClassListCriterion(element: Element, expectedValue: String,
matchFound = classListArray.contains(expectedValue)
case .contains:
matchFound = classListArray.contains { $0.localizedCaseInsensitiveContains(expectedValue) }
case .containsAny:
let expectedParts = expectedValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
matchFound = classListArray.contains { actualPart in
expectedParts.contains { expectedPart in actualPart.localizedCaseInsensitiveContains(expectedPart) }
}
case .prefix:
matchFound = classListArray.contains { $0.hasPrefix(expectedValue) }
case .suffix:
matchFound = classListArray.contains { $0.hasSuffix(expectedValue) }
case .regex:
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "SearchCrit/DOMClass: Regex match type not yet implemented for array. Defaulting to false."))
matchFound = false
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: Regex matching for array of classes. Element: \(elementDescriptionForLog) Expected: \(expectedValue)."))
matchFound = classListArray.contains { item in
if let _ = item.range(of: expectedValue, options: .regularExpression) { return true }
return false
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \\(elementDescriptionForLog) (Array: \\(classListArray)) match type '\\(matchType.rawValue)\' with '\\(expectedValue)\': \\(matchFound)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) (Array: \(classListArray)) match type '\(matchType.rawValue)' with '\(expectedValue)' resolved to \(matchFound)."))
} else if let classListString = domClassListValue as? String {
let classes = classListString.split(separator: " ").map(String.init)
switch matchType {
case .exact:
matchFound = classListString.split(separator: " ").map(String.init).contains(expectedValue)
matchFound = classes.contains(expectedValue)
case .contains:
matchFound = classListString.localizedCaseInsensitiveContains(expectedValue)
case .containsAny:
let expectedParts = expectedValue.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
matchFound = expectedParts.contains { classListString.localizedCaseInsensitiveContains($0) }
case .prefix:
matchFound = classes.contains { $0.hasPrefix(expectedValue) }
case .suffix:
matchFound = classes.contains { $0.hasSuffix(expectedValue) }
case .regex:
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "SearchCrit/DOMClass: Regex match type not yet implemented for string. Defaulting to false."))
matchFound = false
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: Regex matching for space-separated class string. Element: \(elementDescriptionForLog) Expected: \(expectedValue)."))
matchFound = classes.contains { item in
if let _ = item.range(of: expectedValue, options: .regularExpression) { return true }
return false
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \\(elementDescriptionForLog) (String: '\\(classListString)\') match type '\\(matchType.rawValue)\' with '\\(expectedValue)\': \\(matchFound)."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) (String: '\(classListString)') match type '\(matchType.rawValue)' with '\(expectedValue)' resolved to \(matchFound)."))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \\(elementDescriptionForLog) attribute was not [String] or String. Type: \\(type(of: domClassListValue)). No match."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) attribute was not [String] or String (type: \(type(of: domClassListValue))). No match."))
return false
}
if matchFound {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) MATCHED expected '\(expectedValue)' with type '\(matchType.rawValue)'. Classes: '\(domClassListValue)'"))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/DOMClass: \(elementDescriptionForLog) MISMATCHED expected '\(expectedValue)' with type '\(matchType.rawValue)'. Classes: '\(domClassListValue)'"))
}
return matchFound
}
// MARK: - Computed Name Matcher Helper
@MainActor
private func matchComputedNameAttributes(
element: Element,
expectedValue: String,
matchType: JSONPathHintComponent.MatchType,
attributeName: String,
elementDescriptionForLog: String,
includeValueInComputedName: Bool = false
) async -> Bool {
private func matchComputedNameAttributes(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, attributeName: String, elementDescriptionForLog: String, includeValueInComputedName: Bool = false) -> Bool {
let computedName = element.computedName()
if includeValueInComputedName {
// For computedNameWithValue, we might need to include the value attribute
if let value = element.value() as? String {
let combinedName = (computedName ?? "") + " " + value
return compareStrings(combinedName, expectedValue, matchType, attributeName: attributeName, elementDescriptionForLog: elementDescriptionForLog)
@ -241,32 +300,110 @@ private func matchComputedNameAttributes(
return compareStrings(computedName, expectedValue, matchType, attributeName: attributeName, elementDescriptionForLog: elementDescriptionForLog)
}
// MARK: - Value Comparison Helper
// MARK: - String Comparison Logic
@MainActor
internal func compareStrings(_ actual: String?, _ expected: String, _ matchType: JSONPathHintComponent.MatchType, attributeName: String, elementDescriptionForLog: String) -> Bool {
guard let actual = actual else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: '\\(attributeName)\' on \\(elementDescriptionForLog): Actual value is nil. Expected '\\(expected)'. No match."))
return false
public func compareStrings(
_ actualValueOptional: String?,
_ expectedValue: String,
_ matchType: JSONPathHintComponent.MatchType,
caseSensitive: Bool = true,
attributeName: String,
elementDescriptionForLog: String
) -> Bool {
guard let actualValue = actualValueOptional, !actualValue.isEmpty else {
let isEmptyMatch = expectedValue.isEmpty && matchType == .exact
if isEmptyMatch {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: '\(attributeName)' on \(elementDescriptionForLog): Actual is nil/empty, Expected is empty. MATCHED with type '\(matchType.rawValue)'."))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) (actual: nil/empty, expected: '\(expectedValue)', type: \(matchType.rawValue)) -> MISMATCH"))
return false
}
}
let comparisonResult: Bool
let finalActual = caseSensitive ? actualValue : actualValue.lowercased()
let finalExpected = caseSensitive ? expectedValue : expectedValue.lowercased()
var result = false
switch matchType {
case .exact:
comparisonResult = (actual == expected)
result = (finalActual.localizedCompare(finalExpected) == .orderedSame)
case .contains:
comparisonResult = actual.localizedCaseInsensitiveContains(expected)
result = finalActual.contains(finalExpected)
case .regex:
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "SearchCrit/Compare: Regex match type not yet implemented for attribute '\\(attributeName)\'. Defaulting to false."))
comparisonResult = false
result = (finalActual.range(of: finalExpected, options: .regularExpression) != nil)
case .prefix:
result = finalActual.hasPrefix(finalExpected)
case .suffix:
result = finalActual.hasSuffix(finalExpected)
case .containsAny:
let expectedSubstrings = finalExpected.split(separator: ",").map { String($0.trimmingCharacters(in: .whitespacesAndNewlines)) }
if expectedSubstrings.isEmpty && finalActual.isEmpty {
result = true
} else {
result = expectedSubstrings.contains { substring in
finalActual.contains(substring)
}
}
}
if comparisonResult {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: '\\(attributeName)\' on \\(elementDescriptionForLog): Actual ('\\(actual)\') MATCHED Expected ('\\(expected)\') with type '\\(matchType.rawValue)\'."))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: '\\(attributeName)\' on \\(elementDescriptionForLog): Actual ('\\(actual)\') MISMATCHED Expected ('\\(expected)\') with type '\\(matchType.rawValue)\'."))
}
return comparisonResult
let matchStatus = result ? "MATCH" : "MISMATCH"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) (actual: '\(actualValue)', expected: '\(expectedValue)', type: \(matchType.rawValue), caseSensitive: \(caseSensitive)) -> \(matchStatus)"))
return result
}
// MARK: - Functions using undefined types (SearchCriteria, ProcessMatcherProtocol)
// These functions are commented out until the required types are defined
/*
@MainActor
public func evaluateElementAgainstCriteria(
_ element: Element,
criteria: SearchCriteria,
appIdentifier: String?,
processMatcher: ProcessMatcherProtocol
) -> (isMatch: Bool, logs: [AXLogEntry]) {
var logs: [AXLogEntry] = [] // Changed from axDebugLog to aggregated logs
// Check if the app identifier matches, if provided and different from current app
if let criteriaAppIdentifier = criteria.appIdentifier,
let currentAppIdentifier = appIdentifier,
criteriaAppIdentifier != currentAppIdentifier
{
logs.append(AXLogEntry(level: .debug, message: "SearchCriteriaUtils: Element \(element.briefDescription(option: ValueFormatOption.smart)) app mismatch. Criteria wants '\(criteriaAppIdentifier)', current is '\(currentAppIdentifier)'. No match."))
return (false, logs) // Early exit if app ID doesn't match
}
// Check basic properties first (role, subrole, identifier, title, value using direct attribute calls)
if let criteriaRole = criteria.role, element.role() != criteriaRole { // role() is sync
logs.append(AXLogEntry(level: .debug, message: "SearchCriteriaUtils: Element \(element.briefDescription(option: ValueFormatOption.smart)) role mismatch. Expected '\(criteriaRole)', got '\(element.role() ?? "nil")'."))
return (false, logs)
}
// If all checks passed
logs.append(AXLogEntry(level: .debug, message: "SearchCriteriaUtils: Element \(element.briefDescription(option: ValueFormatOption.smart)) matches all criteria for app '\(appIdentifier ?? "any")'."))
return (true, logs)
}
@MainActor
public func elementMatchesAnyCriteria(
_ element: Element,
criteriaList: [SearchCriteria],
appIdentifier: String?,
processMatcher: ProcessMatcherProtocol
) -> (isMatch: Bool, logs: [AXLogEntry]) {
var overallLogs: [AXLogEntry] = []
for criteria in criteriaList {
let result = evaluateElementAgainstCriteria(element, criteria: criteria, appIdentifier: appIdentifier, processMatcher: processMatcher)
overallLogs.append(contentsOf: result.logs)
if result.isMatch {
overallLogs.append(AXLogEntry(level: .debug, message: "SearchCriteriaUtils: Element \(element.briefDescription(option: ValueFormatOption.smart)) matched one of the criteria for app '\(appIdentifier ?? "any")'."))
return (true, overallLogs)
}
}
overallLogs.append(AXLogEntry(level: .debug, message: "SearchCriteriaUtils: Element \(element.briefDescription(option: ValueFormatOption.smart)) did not match any of the criteria for app '\(appIdentifier ?? "any")'."))
return (false, overallLogs)
}
*/

View File

@ -0,0 +1,176 @@
import ApplicationServices
import CoreFoundation
import Foundation
@MainActor
public class AXObserverManager {
// Singleton instance
public static let shared = AXObserverManager()
// Typealias for notification callback - matches AXObserverCallbackWithInfo but without refcon
public typealias AXNotificationCallback = (AXObserver, AXUIElement, CFString, CFDictionary?) -> Void
// Error types
public enum ObserverError: Error {
case couldNotCreateObserver
case addNotificationFailed(AXError)
case other(String)
}
// Private storage for observers and callbacks
private struct ObserverInfo {
let observer: AXObserver
let runLoopSource: CFRunLoopSource
var callbacks: [CFString: AXNotificationCallback] = [:]
}
private var observers: [ObjectIdentifier: ObserverInfo] = [:]
private let observerLock = NSLock()
private init() {}
// Add observer for an element and notification
public func addObserver(for element: Element, notification: AXNotification, callback: @escaping AXNotificationCallback) throws {
let elementId = ObjectIdentifier(element.underlyingElement as AnyObject)
observerLock.lock()
defer { observerLock.unlock() }
// Check if we already have an observer for this element
if var observerInfo = observers[elementId] {
// Add the callback for this notification
observerInfo.callbacks[notification.rawValue as CFString] = callback
observers[elementId] = observerInfo
// Add the notification to the existing observer
let error = AXObserverAddNotification(observerInfo.observer, element.underlyingElement, notification.rawValue as CFString, nil)
if error != .success {
axErrorLog("Failed to add notification: \(error)")
throw ObserverError.addNotificationFailed(error)
}
} else {
// Create a new observer
guard let pid = element.pid() else {
throw ObserverError.other("Could not get PID for element")
}
var observer: AXObserver?
// Create the callback function for the observer
let observerCallback: AXObserverCallbackWithInfo = { (observer, element, notification, userInfo, refcon) in
// Since we can't pass refcon through AXObserverCreateWithInfoCallback,
// we need to use a different approach to get back to the manager
AXObserverManager.shared.handleNotification(observer: observer, element: element, notification: notification, userInfo: userInfo)
}
let error = AXObserverCreateWithInfoCallback(
pid,
observerCallback,
&observer
)
guard error == .success, let observer = observer else {
axErrorLog("Failed to create observer: \(error)")
throw ObserverError.couldNotCreateObserver
}
// Add the notification
let addError = AXObserverAddNotification(observer, element.underlyingElement, notification.rawValue as CFString, nil)
if addError != .success {
axErrorLog("Failed to add notification: \(addError)")
throw ObserverError.addNotificationFailed(addError)
}
// Get the run loop source and add it to the main run loop
let runLoopSource = AXObserverGetRunLoopSource(observer)
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .defaultMode)
// Store the observer info
var callbacks: [CFString: AXNotificationCallback] = [:]
callbacks[notification.rawValue as CFString] = callback
let observerInfo = ObserverInfo(
observer: observer,
runLoopSource: runLoopSource,
callbacks: callbacks
)
observers[elementId] = observerInfo
axDebugLog("Created observer for PID \(pid) with notification \(notification.rawValue)")
}
}
// Remove observer for an element and notification
public func removeObserver(for element: Element, notification: AXNotification) throws {
let elementId = ObjectIdentifier(element.underlyingElement as AnyObject)
observerLock.lock()
defer { observerLock.unlock() }
guard var observerInfo = observers[elementId] else {
// No observer for this element
return
}
// Remove the notification from the observer
let error = AXObserverRemoveNotification(observerInfo.observer, element.underlyingElement, notification.rawValue as CFString)
if error != .success {
axErrorLog("Failed to remove notification: \(error)")
throw ObserverError.other("Failed to remove notification: \(error)")
}
// Remove the callback
observerInfo.callbacks.removeValue(forKey: notification.rawValue as CFString)
// If no more callbacks, remove the observer entirely
if observerInfo.callbacks.isEmpty {
// Remove from run loop
CFRunLoopRemoveSource(CFRunLoopGetMain(), observerInfo.runLoopSource, .defaultMode)
// Invalidate the observer
CFRunLoopSourceInvalidate(observerInfo.runLoopSource)
// Remove from our storage
observers.removeValue(forKey: elementId)
axDebugLog("Removed observer for element")
} else {
// Update the observer info with removed callback
observers[elementId] = observerInfo
}
}
// Handle incoming notifications
private func handleNotification(observer: AXObserver, element: AXUIElement, notification: CFString, userInfo: CFDictionary?) {
let elementId = ObjectIdentifier(element as AnyObject)
observerLock.lock()
let observerInfo = observers[elementId]
observerLock.unlock()
guard let observerInfo = observerInfo,
let callback = observerInfo.callbacks[notification] else {
axWarningLog("Received notification '\(notification)' but no callback found")
return
}
// Call the callback
callback(observer, element, notification, userInfo)
}
// Remove all observers
public func removeAllObservers() {
observerLock.lock()
defer { observerLock.unlock() }
for (_, observerInfo) in observers {
// Remove from run loop
CFRunLoopRemoveSource(CFRunLoopGetMain(), observerInfo.runLoopSource, .defaultMode)
// Invalidate the observer
CFRunLoopSourceInvalidate(observerInfo.runLoopSource)
}
observers.removeAll()
axDebugLog("Removed all observers")
}
}

View File

@ -4,51 +4,66 @@ import ApplicationServices
import Foundation
@MainActor
func extractTextFromElement(_ element: Element, maxDepth: Int = 2, currentDepth: Int = 0) async -> String? {
// Basic attributes first
var components: [String] = []
if let title: String = element.attribute(Attribute<String>.title) { components.append(title) }
if let value: String = element.attribute(Attribute<String>(AXAttributeNames.kAXValueAttribute)) { components.append(value) }
if let description: String = element.attribute(Attribute<String>.description) { components.append(description) }
if let placeholder: String = element.attribute(Attribute<String>.placeholderValue) { components.append(placeholder) }
if let help: String = element.attribute(Attribute<String>.help) { components.append(help) }
// If we have some text, or we've reached max depth, return
if !components.isEmpty || currentDepth >= maxDepth {
let joinedText = components.filter { !$0.isEmpty }.joined(separator: " ")
if !joinedText.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction: Found text '\(joinedText)' at depth \(currentDepth) for element \(element.briefDescription(option: .smart))"))
}
return joinedText.isEmpty ? nil : joinedText
public func extractTextFromElement(_ element: Element, maxDepth: Int = 5, currentDepth: Int = 0) -> String? {
if currentDepth > maxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "extractTextFromElement: Max depth reached for element: \(element.briefDescription(option: ValueFormatOption.smart))"))
return nil
}
// Recursively check children if no text found yet and depth allows
if let children = element.children() {
// Attempt to get text from common attributes
if let title = element.title(), !title.isEmpty { return title }
if let value = element.value() as? String, !value.isEmpty { return value }
if let description = element.descriptionText(), !description.isEmpty { return description }
if let help = element.help(), !help.isEmpty { return help }
// If no direct text, try children
var childrenText: [String] = []
if let children = element.children() { // children() is now synchronous
for child in children {
if let childText = await extractTextFromElement(child, maxDepth: maxDepth, currentDepth: currentDepth + 1) {
components.append(childText)
if let childText = extractTextFromElement(child, maxDepth: maxDepth, currentDepth: currentDepth + 1) { // Removed await
childrenText.append(childText)
}
}
}
let finalText = components.filter { !$0.isEmpty }.joined(separator: " ")
if !finalText.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction: Aggregated text '\(finalText)' at depth \(currentDepth) for element \(element.briefDescription(option: .smart))"))
if !childrenText.isEmpty {
return childrenText.joined(separator: " ")
}
return finalText.isEmpty ? nil : finalText
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "extractTextFromElement: No text found for element: \(element.briefDescription(option: ValueFormatOption.smart))"))
return nil
}
@MainActor
public func extractTextFromElementNonRecursive(_ element: Element) -> String? {
// Try attributes that often hold primary text
if let title = element.title(), !title.isEmpty { return title }
if let value = element.value() as? String, !value.isEmpty { return value }
if let description = element.descriptionText(), !description.isEmpty { return description }
// Fallback to a broader set if primary ones fail
// if let placeholder = element.placeholderValue(), !placeholder.isEmpty { return placeholder }
if let help = element.help(), !help.isEmpty { return help }
// Consider role description as a last resort if it's textual and meaningful
// This might be too generic in many cases, so it's lower priority.
// let roleDesc = element.roleDescription()
// if let roleDesc = roleDesc, !roleDesc.isEmpty { return roleDesc }
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "extractTextFromElementNonRecursive: No direct text found for element: \(element.briefDescription(option: ValueFormatOption.smart))"))
return nil
}
// More focused text extraction, typically used by handlers.
@MainActor
func getElementTextualContent(element: Element, includeChildren: Bool = false, maxDepth: Int = 1, currentDepth: Int = 0) async -> String? {
func getElementTextualContent(element: Element, includeChildren: Bool = false, maxDepth: Int = 1, currentDepth: Int = 0) -> String? {
var textPieces: [String] = []
// Prioritize attributes common for text content
if let title: String = element.attribute(Attribute<String>.title) { textPieces.append(title) }
if let value: String = element.attribute(Attribute<String>(AXAttributeNames.kAXValueAttribute)) { textPieces.append(value) }
if let description: String = element.attribute(Attribute<String>.description) { textPieces.append(description) }
if let placeholder: String = element.attribute(Attribute<String>.placeholderValue) { textPieces.append(placeholder) }
// if let placeholder: String = element.attribute(Attribute<String>.placeholderValue) { textPieces.append(placeholder) }
// Less common but potentially useful
// if let help: String = element.attribute(Attribute.help) { textPieces.append(help) }
// if let selectedText: String = element.attribute(Attribute.selectedText) { textPieces.append(selectedText) }
@ -59,7 +74,8 @@ func getElementTextualContent(element: Element, includeChildren: Bool = false, m
if let children = element.children() {
var childTexts: [String] = []
for child in children {
if let childTextContent = await getElementTextualContent(element: child, includeChildren: true, maxDepth: maxDepth, currentDepth: currentDepth + 1) {
// Recursive call is now synchronous
if let childTextContent = getElementTextualContent(element: child, includeChildren: true, maxDepth: maxDepth, currentDepth: currentDepth + 1) {
childTexts.append(childTextContent)
}
}
@ -80,10 +96,10 @@ func getElementTextualContent(element: Element, includeChildren: Bool = false, m
}
if !joinedDirectText.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction/Content: Extracted '\(joinedDirectText)' for element \(element.briefDescription(option: .smart)) (children included: \(includeChildren), depth: \(currentDepth))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction/Content: Extracted '\(joinedDirectText)' for element \(element.briefDescription(option: ValueFormatOption.smart)) (children included: \(includeChildren), depth: \(currentDepth))"))
return joinedDirectText
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction/Content: No direct text found for \(element.briefDescription(option: .smart)) (children included: \(includeChildren), depth: \(currentDepth))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "TextExtraction/Content: No direct text found for \(element.briefDescription(option: ValueFormatOption.smart)) (children included: \(includeChildren), depth: \(currentDepth))"))
return nil
}

View File

@ -1,230 +0,0 @@
// TreeTraversal.swift - Defines protocols and classes for traversing the accessibility tree.
import ApplicationServices
import Foundation
// GlobalAXLogger is assumed available
// MARK: - Tree Traversal Protocols and Classes
// Protocol for a visitor that processes elements during traversal.
@MainActor
public protocol TreeVisitor {
// Called for each element visited.
func visit(element: Element, depth: Int, state: inout TraversalState) async -> TraversalAction
}
// Represents the result of a visitor's processing of an element.
public enum TraversalAction {
case continueTraversal // Continue traversing children and siblings.
case stop // Stop traversal immediately.
case found(Element) // Element found, stop traversal.
}
// Holds the current state of a traversal (e.g., depth).
public struct TraversalState {
public var currentDepth: Int
public let maxDepth: Int
public var elementsProcessed: Int
public var branchesPruned: Int
public let startTime: Date
public let startElement: Element // The element from which traversal began.
public let strictChildren: Bool // Whether to use strict children mode
public init(maxDepth: Int, startElement: Element, strictChildren: Bool = false) {
self.currentDepth = 0
self.maxDepth = maxDepth
self.elementsProcessed = 0
self.branchesPruned = 0
self.startTime = Date()
self.startElement = startElement
self.strictChildren = strictChildren
}
// Method to check if max depth has been exceeded.
public func shouldStopForDepth() -> Bool {
return currentDepth >= maxDepth
}
public mutating func incrementProcessedCount() {
elementsProcessed += 1
}
public mutating func incrementPrunedCount() {
branchesPruned += 1
}
}
// REMOVED: TreeTraverser class - keeping the struct version below which is more complete
// REMOVED: CollectAllVisitor class - keeping the more complete version below
// This is the actual data structure that CollectAllVisitor.collectedElements will contain.
// Moved here from CollectAllHandler as it's part of the traversal output.
public struct AXElementData: Codable {
public var path: [String]?
public var attributes: [String: AnyCodable]
public var role: String?
public var computedName: String?
}
// MARK: - Unified Tree Traverser
@MainActor
public struct TreeTraverser {
private var visitedElements: Set<Element> = []
public init() {}
public mutating func traverse(from startNode: Element, visitor: TreeVisitor, state: inout TraversalState) async -> Element? {
let startNodeDesc = startNode.briefDescription(option: .smart)
let logMaxDepth = state.maxDepth // Capture value for logging
let logStrictChildren = state.strictChildren // Capture value for logging
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[Traverse Entry] TreeTraverser.traverse starting from: \(startNodeDesc). MaxDepth: \(logMaxDepth), StrictChildren: \(logStrictChildren)"))
visitedElements.removeAll()
return await _traverse(currentElement: startNode, depth: 0, visitor: visitor, state: &state)
}
private mutating func _traverse(currentElement: Element, depth: Int, visitor: TreeVisitor, state: inout TraversalState) async -> Element? {
let currentDesc = currentElement.briefDescription(option: .smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[_Traverse Entry] Visiting \(currentDesc) at depth \(depth)"))
if depth > state.maxDepth {
let maxDepth = state.maxDepth
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Max depth (\(maxDepth)) reached at \(currentElement.briefDescription(option: .raw))"))
return nil
}
if visitedElements.contains(currentElement) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Cycle detected at \(currentElement.briefDescription(option: .raw)). Skipping."))
return nil
}
visitedElements.insert(currentElement)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Visiting \(currentElement.briefDescription(option: .raw)) at depth \(depth)"))
switch await visitor.visit(element: currentElement, depth: depth, state: &state) {
case .found(let foundElement):
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Element found by visitor: \(foundElement.briefDescription(option: .raw))"))
return foundElement
case .stop:
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Traversal stopped by visitor at \(currentElement.briefDescription(option: .raw))"))
return nil
case .continueTraversal:
break
}
guard let children = currentElement.children(strict: state.strictChildren) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "No children for \(currentElement.briefDescription(option: .raw)) or error fetching them."))
return nil
}
for child in children {
if let found = await _traverse(currentElement: child, depth: depth + 1, visitor: visitor, state: &state) {
return found
}
}
return nil
}
}
// MARK: - Visitor Implementations
@MainActor
public class CollectAllVisitor: TreeVisitor {
private let attributesToFetch: [String]
private let outputFormat: OutputFormat
private let appElement: Element // Used for path generation relative to app root
public var collectedElements: [AXElementData] = []
// Default valueFormatOption for CollectAllVisitor if not specified otherwise
private let valueFormatOption: ValueFormatOption
private let filterCriteria: [Criterion]?
public init(attributesToFetch: [String], outputFormat: OutputFormat, appElement: Element, valueFormatOption: ValueFormatOption = .smart, filterCriteria: [Criterion]? = nil) {
self.attributesToFetch = attributesToFetch
self.outputFormat = outputFormat
self.appElement = appElement
self.valueFormatOption = valueFormatOption
self.filterCriteria = filterCriteria
}
// Convenience initializer for backward compatibility with dictionary
public convenience init(attributesToFetch: [String], outputFormat: OutputFormat, appElement: Element, valueFormatOption: ValueFormatOption = .smart, filterCriteria: [String: String]?) {
let criterionArray = filterCriteria?.map { key, value in
Criterion(attribute: key, value: value, match_type: nil)
}
self.init(attributesToFetch: attributesToFetch, outputFormat: outputFormat, appElement: appElement, valueFormatOption: valueFormatOption, filterCriteria: criterionArray)
}
public func visit(element: Element, depth: Int, state: inout TraversalState) async -> TraversalAction {
if let criteria = self.filterCriteria, !criteria.isEmpty {
let matchesFilter = await elementMatchesCriteria(element, criteria: criteria)
if !matchesFilter {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[CollectAllVisitor] Element \(element.briefDescription(option: .raw)) did NOT match filterCriteria. Skipping."))
return .continueTraversal
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "[CollectAllVisitor] Element \(element.briefDescription(option: .raw)) MATCHED filterCriteria."))
}
let (fetchedAttrs, _) = await getElementAttributes(
element: element,
attributes: attributesToFetch,
outputFormat: outputFormat,
valueFormatOption: self.valueFormatOption
)
let elementPath = element.generatePathArray(upTo: appElement)
let role = element.role()
let compName = element.computedName()
let elementData = AXElementData(
path: elementPath,
attributes: fetchedAttrs,
role: role,
computedName: compName
)
collectedElements.append(elementData)
return .continueTraversal
}
}
@MainActor
public class SearchVisitor: TreeVisitor {
private let locator: Locator
private let requireAction: String?
private var foundElement: Element?
private var elementsProcessed: Int = 0
public init(locator: Locator, requireAction: String? = nil) {
self.locator = locator
self.requireAction = requireAction
}
public func visit(element: Element, depth: Int, state: inout TraversalState) async -> TraversalAction {
elementsProcessed += 1
if foundElement != nil {
return .stop
}
if depth == 0 && elementsProcessed == 1 {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchVisitor: Starting new search. Locator: \(self.locator.criteria)"))
}
let matchStatus = await evaluateElementAgainstCriteria(
element: element,
locator: locator,
actionToVerify: requireAction ?? locator.requireAction,
depth: depth
)
switch matchStatus {
case .fullMatch:
foundElement = element
return .found(element)
case .noMatch, .partialMatchActionMissing:
return .continueTraversal
}
}
}
// REMOVED: dLog global helper - use GlobalAXLogger directly.
// REMOVED: Element extensions for briefDescriptionForDebug and pidString - use refactored Element methods.

View File

@ -52,6 +52,9 @@ struct ValueUnwrapper {
let axVal = value as! AXValue
let axValueType = axVal.valueType
// Log the AXValueType
axDebugLog("ValueUnwrapper.unwrapAXValue: Encountered AXValue with type: \(axValueType) (rawValue: \(axValueType.rawValue))")
// Handle special boolean type
if axValueType.rawValue == 4 { // kAXValueBooleanType (private)
var boolResult: DarwinBoolean = false
@ -61,7 +64,9 @@ struct ValueUnwrapper {
}
// Use new AXValue extensions for cleaner unwrapping
return axVal.value()
let unwrappedExtensionValue = axVal.value()
axDebugLog("ValueUnwrapper.unwrapAXValue: axVal.value() returned: \(String(describing: unwrappedExtensionValue)) for type: \(axValueType)")
return unwrappedExtensionValue
}
@MainActor

View File

@ -1,15 +1,19 @@
// AXORCMain.swift - Main entry point for AXORC CLI
import ArgumentParser
import AXorcist // For AXorcist instance
import Foundation
import CoreFoundation
@preconcurrency import ArgumentParser
import AXorcist // For AXorcist instance
// axorcVersion is now defined in AXORCModels.swift
// let axorcVersion = "0.1.0-dev"
@main
struct AXORCCommand: AsyncParsableCommand {
struct AXORCCommand: @preconcurrency ParsableCommand {
static let configuration: CommandConfiguration = CommandConfiguration(
commandName: "axorc",
abstract: "AXORC CLI - Handles JSON commands via various input methods. Version \(axorcVersion)"
// Use axorcVersion from AXORCModels.swift or a shared constant place
abstract: "AXORC CLI - Handles JSON commands via various input methods. Version \\(axorcVersion)"
)
@Flag(name: .long, help: "Enable debug logging for the command execution.")
@ -30,12 +34,13 @@ struct AXORCCommand: AsyncParsableCommand {
var directPayload: String?
// Helper function to process and execute a CommandEnvelope
private func processAndExecuteCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) async {
@MainActor
private func processAndExecuteCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) {
if debugCLI {
axDebugLog("Successfully parsed command: \(command.command) (ID: \(command.commandId))")
}
let resultJsonString = await CommandExecutor.execute(
let resultJsonString = CommandExecutor.execute(
command: command,
axorcist: axorcist,
debugCLI: debugCLI
@ -47,9 +52,9 @@ struct AXORCCommand: AsyncParsableCommand {
var observerSetupSucceeded = false
if let resultData = resultJsonString.data(using: .utf8) {
do {
if let json = try JSONSerialization.jsonObject(with: resultData, options: []) as? [String: Any],
let success = json["success"] as? Bool,
let status = json["status"] as? String {
if let jsonOutput = try JSONSerialization.jsonObject(with: resultData, options: []) as? [String: Any],
let success = jsonOutput["success"] as? Bool,
let status = jsonOutput["status"] as? String {
axInfoLog("AXORCMain: Parsed initial response for observe: success=\(success), status=\(status)")
if success && status == "observer_started" {
observerSetupSucceeded = true
@ -68,34 +73,43 @@ struct AXORCCommand: AsyncParsableCommand {
}
if observerSetupSucceeded {
axInfoLog("AXORCMain: Observer setup successful. Process will remain alive.")
axInfoLog("AXORCMain: Observer setup successful. Process will remain alive by running current RunLoop.")
#if DEBUG
axInfoLog("AXORCMain: DEBUG mode - launching dedicated run-loop thread for observer.")
Thread.detachNewThread { CFRunLoopRun() }
axInfoLog("AXORCMain: DEBUG mode - main task entering infinite sleep loop.")
while true {
do { try await Task.sleep(nanoseconds: 3_600_000_000_000) }
catch { axInfoLog("AXORCMain: Main task sleep interrupted."); break }
}
axInfoLog("AXORCMain: DEBUG mode - entering RunLoop.current.run() for observer.")
RunLoop.current.run()
axInfoLog("AXORCMain: DEBUG mode - RunLoop.current.run() finished.")
#else
fputs("{\"error\": \"The 'observe' command is intended for DEBUG builds or specific use cases and will not run indefinitely in this release build. Exiting.\"}\n", stderr)
fputs("{\"error\": \"The 'observe' command is intended for DEBUG builds or specific use cases. In release, it sets up the observer but will not keep the process alive indefinitely by itself. Exiting normally after setup.\"}\n", stderr)
fflush(stderr)
Foundation.exit(EXIT_FAILURE)
#endif
} else {
axErrorLog("AXORCMain: Observe command setup reported failure or result was not a success status. Exiting.")
}
} else {
axClearLogs() // Clear logs for non-observe commands after execution
axClearLogs()
}
}
mutating func run() async throws {
// Configure GlobalAXLogger based on debug flag
await GlobalAXLogger.shared.setLoggingEnabled(debug)
await GlobalAXLogger.shared.setDetailLevel(debug ? .verbose : .minimal)
@MainActor
mutating func run() throws {
fputs("AXORCMain.run: VERY FIRST LINE EXECUTED.\n", stderr)
fflush(stderr)
GlobalAXLogger.shared.isLoggingEnabled = debug
GlobalAXLogger.shared.detailLevel = debug ? .verbose : .minimal
// Confirm settings
fputs("AXORCMain.run: CLI --debug flag is: \(debug). Logger enabled: \(GlobalAXLogger.shared.isLoggingEnabled).\n", stderr)
fflush(stderr)
// <<< TEST LOGGING START >>>
axErrorLog("AXORCMain.run: TEST ERROR LOG -- SHOULD ALWAYS APPEAR IN DEBUG OUTPUT IF LOGS ARE PRINTED")
axDebugLog("AXORCMain.run: TEST DEBUG LOG -- SHOULD APPEAR IF CLI --debug IS ON")
if debug {
fputs("AXORCMain.run: STDERR - CLI --debug IS ON. TEST LOGGING.\n", stderr)
}
// <<< TEST LOGGING END >>>
// Parse input using InputHandler
let inputResult = InputHandler.parseInput(
stdin: stdin,
file: file,
@ -103,9 +117,10 @@ struct AXORCCommand: AsyncParsableCommand {
directPayload: directPayload
)
// Handle input errors
let axorcistInstance = AXorcist.shared // Use the shared instance
if let error = inputResult.error {
let collectedLogs = debug ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true) : nil
let collectedLogs = debug ? GlobalAXLogger.shared.getLogsAsStrings(format: .text) : nil
let errorResponse = ErrorResponse(commandId: "input_error", error: error, debugLogs: collectedLogs)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
@ -116,7 +131,7 @@ struct AXORCCommand: AsyncParsableCommand {
}
guard let jsonStringFromInput = inputResult.jsonString else {
let collectedLogs = debug ? await GlobalAXLogger.shared.getLogsAsStrings(format: .text, includeTimestamps: true, includeLevels: true, includeDetails: true) : nil
let collectedLogs = debug ? GlobalAXLogger.shared.getLogsAsStrings(format: .text) : nil
let errorResponse = ErrorResponse(commandId: "no_input", error: "No valid JSON input received", debugLogs: collectedLogs)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonStr = String(data: jsonData, encoding: .utf8) {
print(jsonStr)
@ -128,37 +143,90 @@ struct AXORCCommand: AsyncParsableCommand {
axDebugLog("AXORCMain Test: Received jsonStringFromInput: [\(jsonStringFromInput)] (length: \(jsonStringFromInput.count))")
if let data = jsonStringFromInput.data(using: .utf8) {
let axorcist = AXorcist()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
// Attempt 1: Decode as [CommandEnvelope]
let commands = try decoder.decode([CommandEnvelope].self, from: data)
if let command = commands.first {
axDebugLog("AXORCMain Test: Decode attempt 1: Successfully decoded [CommandEnvelope] and got first command.")
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
processAndExecuteCommand(command: command, axorcist: axorcistInstance, debugCLI: debug)
} else {
axDebugLog("AXORCMain Test: Decode attempt 1: Decoded [CommandEnvelope] but array was empty.")
// Create a generic error to throw if this path is problematic
let anError = NSError(domain: "AXORCErrorDomain", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Decoded empty command array from [CommandEnvelope] attempt."])
throw anError
}
} catch let arrayDecodeError {
axDebugLog("AXORCMain Test: Decode attempt 1 (as [CommandEnvelope]) FAILED. Error: \(arrayDecodeError). Will try as single CommandEnvelope.")
// Attempt 2: Decode as single CommandEnvelope
do {
let command = try decoder.decode(CommandEnvelope.self, from: data) // data is still from jsonStringFromInput
let command = try decoder.decode(CommandEnvelope.self, from: data)
axDebugLog("AXORCMain Test: Decode attempt 2: Successfully decoded as SINGLE CommandEnvelope.")
await processAndExecuteCommand(command: command, axorcist: axorcist, debugCLI: debug)
processAndExecuteCommand(command: command, axorcist: axorcistInstance, debugCLI: debug)
} catch let singleDecodeError {
axDebugLog("AXORCMain Test: Decode attempt 2 (as single CommandEnvelope) ALSO FAILED. Error: \(singleDecodeError). Original array decode error was: \(arrayDecodeError)")
throw singleDecodeError // Throw the error from the single decode attempt as it's the most direct if input was not an array
let errorResponse = ErrorResponse(commandId: "decode_error", error: "Failed to decode JSON input: \(singleDecodeError.localizedDescription)", debugLogs: debug ? axGetLogsAsStrings() : nil)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonErrorString = String(data: jsonData, encoding: .utf8) {
print(jsonErrorString)
} else {
print("{\"error\": \"Failed to encode decode error response: \(singleDecodeError.localizedDescription)\"}")
}
return
}
}
} else {
axDebugLog("AXORCMain Test: Failed to convert jsonStringFromInput to data.")
let anError = NSError(domain: "AXORCErrorDomain", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to convert jsonStringFromInput to data."])
throw anError
let errorResponse = ErrorResponse(commandId: "data_conversion_error", error: "Failed to convert JSON string to data", debugLogs: debug ? axGetLogsAsStrings() : nil)
if let jsonData = try? JSONEncoder().encode(errorResponse), let jsonErrorString = String(data: jsonData, encoding: .utf8) {
print(jsonErrorString)
} else {
print("{\"error\": \"Failed to encode data conversion error response\"}")
}
return
}
// After processing all commands or if an error occurs
if debug && commandShouldPrintLogsAtEnd() {
let logMessages = axGetLogsAsStrings(format: .text)
if !logMessages.isEmpty {
fputs("\n--- Debug Logs (axorc run end) ---\n", stderr)
for logMessage in logMessages {
fputs(logMessage + "\n", stderr)
}
fputs("--- End Debug Logs ---\n", stderr)
fflush(stderr)
}
}
}
@MainActor
private func commandShouldPrintLogsAtEnd() -> Bool {
// This is a simplified check. A more robust way would be to check
// the actual command type if it's available here.
// For now, if stdin is true or json is provided, assume it might be an observe command.
// This is imperfect.
// Remove try? if InputHandler.parseInput is not throwing
if let jsonString = InputHandler.parseInput(stdin: stdin, file: file, json: json, directPayload: directPayload).jsonString,
let inputData = jsonString.data(using: .utf8) { // Corrected optional chaining and conditional binding
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let commands = try? decoder.decode([CommandEnvelope].self, from: inputData),
commands.first?.command == .observe {
return false
}
if let command = try? decoder.decode(CommandEnvelope.self, from: inputData),
command.command == .observe {
return false
}
}
return true
}
}
// ErrorResponse struct is now defined in AXORCModels.swift
// struct ErrorResponse: Codable {
// var commandId: String
// var status: String = "error"
// var error: String
// var debugLogs: [String]?
// }
// Removed placeholder GlobalAXLogger extension as it was moved/integrated previously

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,13 @@ import Foundation
// MARK: - Version and Configuration
let axorcVersion = "0.1.2a-config_fix"
// MARK: - Shared Error Detail
// Moved ErrorDetail to be a top-level struct
struct ErrorDetail: Codable {
let message: String
}
// MARK: - Response Models
// These should align with structs in AXorcistIntegrationTests.swift
@ -32,9 +39,6 @@ struct SimpleSuccessResponse: Codable {
struct ErrorResponse: Codable {
let commandId: String
var success: Bool = false // Default to false for errors
struct ErrorDetail: Codable {
let message: String
}
let error: ErrorDetail
let debugLogs: [String]?
@ -72,7 +76,7 @@ struct QueryResponse: Codable {
let success: Bool
let command: String // Name of the command, e.g., "getFocusedElement"
let data: AXElementForEncoding? // Contains the AX element's data, adapted for encoding
let error: ErrorResponse.ErrorDetail?
let error: ErrorDetail?
let debugLogs: [String]?
enum CodingKeys: String, CodingKey {
@ -96,7 +100,7 @@ struct QueryResponse: Codable {
self.data = nil
}
if let errorMsg = handlerResponse.error {
self.error = ErrorResponse.ErrorDetail(message: errorMsg)
self.error = ErrorDetail(message: errorMsg)
} else {
self.error = nil
}
@ -116,7 +120,7 @@ struct QueryResponse: Codable {
self.command = command ?? "unknown"
self.data = axElement != nil ? AXElementForEncoding(from: axElement!) : nil
if let errorMessage = error {
self.error = ErrorResponse.ErrorDetail(message: errorMessage)
self.error = ErrorDetail(message: errorMessage)
} else {
self.error = nil
}
@ -138,6 +142,45 @@ struct BatchResponse: Codable {
}
}
// For batch operations that may have mixed results
struct BatchQueryResponse: Codable {
let commandId: String
let status: String
var message: String?
var data: [AnyCodable?]?
var errors: [String]?
var debugLogs: [String]?
enum CodingKeys: String, CodingKey {
case commandId
case status
case message
case data
case errors
case debugLogs
}
}
// Generic query response for commands (renamed to avoid conflict)
struct GenericQueryResponse: Codable {
let commandId: String
let commandType: String
let status: String
let data: AnyCodable?
let message: String?
var debugLogs: [String]?
enum CodingKeys: String, CodingKey {
case commandId
case commandType
case status
case data
case message
case debugLogs
}
}
// Helper for DecodingError display
extension DecodingError {
var humanReadableDescription: String {

15
axorc_stderr.log Normal file
View File

@ -0,0 +1,15 @@
AXORCMain.run: VERY FIRST LINE EXECUTED.
AXORCMain.run: CLI --debug flag is: true. Logger enabled: true.
AXORCMain.run: STDERR - CLI --debug IS ON. TEST LOGGING.
2025-05-27T00:52:06+0200 info AXorcist.ElementSearch : [AXorcist] FindTargetEl: START
App: 'com.todesktop.230313mzl4w4u92'
MaxDepth: 10
Initial Criteria: [AXRole:AXTextArea, match:exact]
PathHint (count: 0):
-> nil
2025-05-27T00:52:11+0200 warning AXorcist.ElementSearch : [AXorcist] FindTargetEl: No element found matching final criteria [AXRole:AXTextArea] starting from application root Role: AXApplication, Title: 'Cursor'.
--- Debug Logs (axorc run end) ---
[2025-05-26T22:54:42.489Z] [DEBUG] [InputHandler.swift:21] [parseInput(stdin:file:json:directPayload:)] - InputHandler: Parsing input...
[2025-05-26T22:54:42.489Z] [DEBUG] [InputHandler.swift:34] [parseInput(stdin:file:json:directPayload:)] - Using --json flag with payload of 720 characters.
--- End Debug Logs ---

18
run_axorc_with_json_content.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Read the entire content of the JSON file into a variable
json_content=$(<"/Users/steipete/Projects/CodeLooper/query_cursor_input.json")
# Extract and print the command_id from the JSON content
command_id=$(echo "$json_content" | jq -r .command_id)
echo "DEBUG: command_id from file is: $command_id"
# Remove any previous debug log file
rm -f axorc_stderr.log
# Execute axorc with the JSON content (not the file path) and debug flag
# Redirect stderr to axorc_stderr.log
./.build/arm64-apple-macosx/debug/axorc --json "$json_content" --debug 2>axorc_stderr.log
# Cat the stderr log content to stdout
cat axorc_stderr.log