break apart large files

This commit is contained in:
Peter Steinberger 2025-05-27 10:07:01 +02:00
parent dacf59720b
commit 789bde944c
30 changed files with 2452 additions and 2553 deletions

View File

@ -36,9 +36,13 @@ let package = Package(
path: "Sources/axorc", // Explicit path
sources: [
"AXORCMain.swift",
"Core/CommandExecutor.swift",
"Core/InputHandler.swift",
"Models/AXORCModels.swift"
"Models/AXORCModels.swift",
"CommandExecutor.swift",
"CommandExecutionFunctions.swift",
"CommandHandlers.swift",
"CommandResponseHelpers.swift",
"CommandTypeExtensions.swift"
]
),
.testTarget(

View File

@ -0,0 +1,256 @@
// AXCommands.swift - AXCommand enum and individual command structs
import CoreGraphics
import Foundation
// 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

@ -8,31 +8,6 @@
import ApplicationServices
import Foundation
/// Callback type for observer notifications
// public typealias AXObserverHandler = @MainActor (pid_t, AXNotification, AXObserver, AXUIElement, CFDictionary?) -> Void // Old handler
/// New callback type for subscriptions. The AXObserver and AXUIElement might be less relevant to the direct subscriber
/// if the Center abstracts them, or they can be added back if deemed necessary.
public typealias AXNotificationSubscriptionHandler = @MainActor (/*element: Element,*/ pid_t, AXNotification, _ rawElement: AXUIElement, _ nsUserInfo: [String: Any]?) -> Void
/// Key for tracking registered notifications. Can allow nil PID for global observers for a specific notification type.
public struct AXNotificationSubscriptionKey: Hashable {
let pid: pid_t? // Optional to allow for global observers for a specific notification
let notification: AXNotification
}
/// Key and PID pair for tracking registered notifications
public struct AXObserverKeyAndPID: Hashable {
let pid: pid_t
let key: AXNotification
}
/// Observer and PID pair for tracking active observers
public struct AXObserverObjAndPID {
var observer: AXObserver
var pid: pid_t
}
/// Centralized manager for AXObserver instances
@MainActor
public class AXObserverCenter {
@ -48,11 +23,6 @@ public class AXObserverCenter {
private var subscriptionTokens: [UUID: AXNotificationSubscriptionKey] = [:]
private let subscriptionsLock = NSLock() // Added subscriptionsLock
/// Public token for unsubscribing
public struct SubscriptionToken: Hashable {
let id: UUID
}
/// Handler to be called when notifications are received - To be replaced by subscriptions model
// public var handler: AXObserverHandler?
@ -148,6 +118,62 @@ public class AXObserverCenter {
}
}
// MARK: - Public Methods
/// Remove all observers and all subscriptions.
@MainActor
public func removeAllObservers() {
axInfoLog("Removing all observers and subscriptions globally.")
subscriptionsLock.lock()
defer { subscriptionsLock.unlock() }
// Unsubscribe all known tokens
for tokenID in subscriptionTokens.keys {
if let key = subscriptionTokens[tokenID] { // Safely unwrap
if var handlersForKey = subscriptions[key] {
handlersForKey.removeValue(forKey: tokenID)
if handlersForKey.isEmpty {
subscriptions.removeValue(forKey: key)
// Potential cleanup of underlying observer if no subscriptions remain for this specific key
cleanupUnderlyingObserverNotification(forPid: key.pid, notification: key.notification)
} else {
subscriptions[key] = handlersForKey
}
}
}
}
subscriptionTokens.removeAll()
// After all unsubscriptions, observers and subscriptions should be empty.
if !self.observers.isEmpty || !self.subscriptions.isEmpty || !self.subscriptionTokens.isEmpty { // Added self.
axWarningLog("removeAllObservers: observers, subscriptions, or tokens list not empty after mass unsubscribe. observers: \(self.observers.count), subscriptions: \(self.subscriptions.count), tokens: \(self.subscriptionTokens.count)") // Added self.
// Force clear for safety, though unsubscribe should handle it.
self.observers.removeAll() // Added self.
self.subscriptions.removeAll() // Added self.
self.subscriptionTokens.removeAll() // Added self.
}
axInfoLog("All observers and subscriptions have been cleared.")
}
/// Remove all observers for a specific process
public func removeAllObservers(for pid: pid_t) {
axInfoLog("Removing all observers and subscriptions for PID \(pid)")
let tokensForPid = subscriptionTokens.filter { $0.value.pid == pid }.map { $0.key }
for tokenId in tokensForPid {
try? unsubscribe(token: SubscriptionToken(id: tokenId))
}
// Also handle global observers that might have been tied to this app if pid was 0 initially
// but that logic is complex and might be better handled by specific unsubscription.
// The current loop handles subscriptions explicitly tied to this PID.
}
/// Check if a notification key is registered for a process
public func isKeyRegistered(pid: pid_t?, notification: AXNotification) -> Bool { // pid is now optional
// return observerKeys.contains { $0.pid == pid && $0.key == notification } // Old way
let key = AXNotificationSubscriptionKey(pid: pid, notification: notification)
return subscriptions[key]?.isEmpty == false
}
// MARK: - Internal AXObserver Management (previously addObserver / removeObserver)
/// Ensures an AXObserver is created for the PID and the notification is added to it.
@ -236,60 +262,6 @@ public class AXObserverCenter {
}
}
/// Remove all observers and all subscriptions.
@MainActor
public func removeAllObservers() {
axInfoLog("Removing all observers and subscriptions globally.")
subscriptionsLock.lock()
defer { subscriptionsLock.unlock() }
// Unsubscribe all known tokens
for tokenID in subscriptionTokens.keys {
if let key = subscriptionTokens[tokenID] { // Safely unwrap
if var handlersForKey = subscriptions[key] {
handlersForKey.removeValue(forKey: tokenID)
if handlersForKey.isEmpty {
subscriptions.removeValue(forKey: key)
// Potential cleanup of underlying observer if no subscriptions remain for this specific key
cleanupUnderlyingObserverNotification(forPid: key.pid, notification: key.notification)
} else {
subscriptions[key] = handlersForKey
}
}
}
}
subscriptionTokens.removeAll()
// After all unsubscriptions, observers and subscriptions should be empty.
if !self.observers.isEmpty || !self.subscriptions.isEmpty || !self.subscriptionTokens.isEmpty { // Added self.
axWarningLog("removeAllObservers: observers, subscriptions, or tokens list not empty after mass unsubscribe. observers: \(self.observers.count), subscriptions: \(self.subscriptions.count), tokens: \(self.subscriptionTokens.count)") // Added self.
// Force clear for safety, though unsubscribe should handle it.
self.observers.removeAll() // Added self.
self.subscriptions.removeAll() // Added self.
self.subscriptionTokens.removeAll() // Added self.
}
axInfoLog("All observers and subscriptions have been cleared.")
}
/// Remove all observers for a specific process
public func removeAllObservers(for pid: pid_t) {
axInfoLog("Removing all observers and subscriptions for PID \(pid)")
let tokensForPid = subscriptionTokens.filter { $0.value.pid == pid }.map { $0.key }
for tokenId in tokensForPid {
try? unsubscribe(token: SubscriptionToken(id: tokenId))
}
// Also handle global observers that might have been tied to this app if pid was 0 initially
// but that logic is complex and might be better handled by specific unsubscription.
// The current loop handles subscriptions explicitly tied to this PID.
}
/// Check if a notification key is registered for a process
public func isKeyRegistered(pid: pid_t?, notification: AXNotification) -> Bool { // pid is now optional
// return observerKeys.contains { $0.pid == pid && $0.key == notification } // Old way
let key = AXNotificationSubscriptionKey(pid: pid, notification: notification)
return subscriptions[key]?.isEmpty == false
}
// MARK: - Private Methods
private func getObserver(for pid: pid_t) -> AXObserver? {
@ -325,7 +297,7 @@ public class AXObserverCenter {
if let cfDict = cfUserInfo as? [CFString: CFTypeRef] {
var tempDict = [String: Any]()
for (key, value) in cfDict {
tempDict[key as String] = center.convertCFValueToSwift(value)
tempDict[key as String] = convertCFValueToSwift(value)
}
nsUserInfo = tempDict
} else {
@ -374,194 +346,11 @@ public class AXObserverCenter {
}
}
// private func removeKey(pid: pid_t, key: AXNotification) { // Old method
// observerKeys.removeAll { $0.pid == pid && $0.key == key }
// }
private func removePidObserverInstance(pid: pid_t) {
observers.removeAll { $0.pid == pid }
axDebugLog("Removed AXObserver instance for effective PID \(pid).")
}
// MARK: - Helper for userInfo conversion
private func convertCFValueToSwift(_ cfValue: CFTypeRef?) -> Any? {
guard let cfValue = cfValue else { return nil }
let typeID = CFGetTypeID(cfValue)
switch typeID {
case CFStringGetTypeID():
return cfValue as? String
case CFNumberGetTypeID():
return cfValue as? NSNumber // Could be Int, Double, Bool (via NSNumber bridging)
case CFBooleanGetTypeID():
// Ensure correct conversion for CFBoolean
if CFEqual(cfValue, kCFBooleanTrue) {
return true
} else if CFEqual(cfValue, kCFBooleanFalse) {
return false
}
// Fallback for other CFBoolean representations if any, or if direct Bool bridging works
if let boolVal = cfValue as? Bool {
return boolVal
}
axWarningLog("Could not convert CFBoolean to Bool: \(String(describing: cfValue))")
return nil // Or handle as error
case CFArrayGetTypeID():
// Swift arrays bridge to CFArray, and CFArray can be cast to NSArray / [AnyObject]
if let cfArray = cfValue as? [CFTypeRef] { // or cfValue as? NSArray
return cfArray.compactMap { convertCFValueToSwift($0) }
}
axWarningLog("Failed to convert CFArray from userInfo.")
return cfValue // Return raw CFArray if conversion fails for some reason
case CFDictionaryGetTypeID():
if let cfDict = cfValue as? [CFString: CFTypeRef] { // or cfValue as? NSDictionary
var swiftDict = [String: Any]()
for (key, value) in cfDict {
swiftDict[key as String] = convertCFValueToSwift(value)
}
return swiftDict
}
axWarningLog("Failed to convert nested CFDictionary from userInfo.")
return cfValue // Return raw CFDictionary if conversion fails
case AXUIElementGetTypeID():
return cfValue as! AXUIElement // Should be safe to force unwrap if type matches
// Add other common CF types if necessary, e.g., CFURL, CFDate
default:
axDebugLog("Unhandled CFTypeRef in convertCFValueToSwift: typeID \(typeID). Value: \(cfValue)")
return cfValue // Return raw CFTypeRef if unhandled, caller might know what to do
}
}
// Actual callback function that receives notifications
private func axObserverCallbackWithInfo(
_ observer: AXObserver!,
_ axElement: AXUIElement!, // Renamed to axElement
_ notification: CFString!,
_ userInfo: CFDictionary?, // This is CFDictionary?, which is correct
_ refcon: UnsafeMutableRawPointer!
) {
let center = Unmanaged<AXObserverCenter>.fromOpaque(refcon).takeUnretainedValue()
guard let axNotification = AXNotification(rawValue: notification as String) else {
axWarningLog("Received unknown notification: \(notification as String)")
return
}
var pid: pid_t = 0
let pidError = AXUIElementGetPid(axElement, &pid)
if pidError != .success {
if let observerInstance = center.observers.first(where: { $0.observer == observer }), observerInstance.pid != 0 {
pid = observerInstance.pid
axDebugLog("AXUIElementGetPid failed for observed element. Using PID from observer instance: \(pid). Notification: \(axNotification.rawValue)")
} else {
axWarningLog("AXUIElementGetPid failed for observed element and could not determine PID. Notification: \(axNotification.rawValue). Error: \(pidError.rawValue)")
}
}
// Convert CFDictionary to [String: Any]?
var nsUserInfo: [String: Any]?
if let cfUserInfo = userInfo {
nsUserInfo = center.convertCFValueToSwift(cfUserInfo) as? [String: Any]
}
// Dispatch to relevant handlers
// Check for PID-specific subscriptions
let specificKey = AXNotificationSubscriptionKey(pid: pid, notification: axNotification)
if let handlers = center.subscriptions[specificKey] {
axDebugLog("Dispatching to \(handlers.count) PID-specific handlers for PID \(pid), notification \(axNotification.rawValue)")
for handler in handlers.values {
handler(pid, axNotification, axElement, nsUserInfo) // Pass raw axElement, handler expects AXUIElement
}
}
// Check for global subscriptions (pid == nil in key)
let globalKey = AXNotificationSubscriptionKey(pid: nil, notification: axNotification)
if let globalHandlers = center.subscriptions[globalKey] {
axDebugLog("Dispatching to \(globalHandlers.count) global handlers for notification \(axNotification.rawValue)")
for handler in globalHandlers.values {
handler(pid, axNotification, axElement, nsUserInfo) // Pass raw axElement, handler expects AXUIElement
}
}
if center.subscriptions[specificKey] == nil && center.subscriptions[globalKey] == nil {
axWarningLog("No handlers found for notification \(axNotification.rawValue).")
}
}
// Global notification callback function
private func axObserverCallback(
_ observer: AXObserver,
_ element: AXUIElement,
_ notificationName: CFString,
_ context: UnsafeMutableRawPointer?
) {
// This is the older, simpler callback without userInfo.
// We will primarily use axObserverCallbackWithInfo if possible.
// However, some observers might still use this one if not configured for info.
guard let context = context else {
axWarningLog("AXObserver callback invoked with nil context.")
return
}
let observerCenter = Unmanaged<AXObserverCenter>.fromOpaque(context).takeUnretainedValue()
let notification = AXNotification(rawValue: notificationName as String) ?? .created // Fallback if unknown
// Get PID from stored observer data instead of AXObserverGetPID (which doesn't exist in Swift)
var appPid: pid_t = 0
if let observerData = observerCenter.observers.first(where: { $0.observer == observer }) {
appPid = observerData.pid
} else {
// Try to get PID from the element if we couldn't find the observer
AXUIElementGetPid(element, &appPid)
}
Task {
observerCenter.processNotification(pid: appPid, notification: notification, rawElement: element, nsUserInfo: nil)
}
}
// Global notification callback function WITH USERINFO
// This is the one we expect to be called by AXObserverAddNotification when userInfo is passed.
private func axObserverCallbackWithInfo(
_ observer: AXObserver, // The AXObserver instance that triggered the callback.
_ element: AXUIElement, // The AXUIElement that the notification is about.
_ notificationName: CFString, // The name of the notification (e.g., kAXFocusedUIElementChangedNotification).
_ userInfo: CFDictionary?, // An optional dictionary containing additional information about the notification.
_ context: UnsafeMutableRawPointer? // User-defined data passed when the observer was registered (self for AXObserverCenter).
) {
guard let context = context else {
axWarningLog("AXObserver callback (with info) invoked with nil context.")
return
}
let observerCenter = Unmanaged<AXObserverCenter>.fromOpaque(context).takeUnretainedValue()
let notification = AXNotification(rawValue: notificationName as String)
guard let axNotification = notification else {
axWarningLog("Received unknown notification: \(notificationName as String)")
return
}
// Get the PID associated with the observer from our stored data
// (AXObserverGetPID doesn't exist in Swift)
var eventPid: pid_t = 0
if let observerData = observerCenter.observers.first(where: { $0.observer == observer }) {
eventPid = observerData.pid
}
// Try to get the PID of the element that the notification is about, which is more relevant.
var elementPid: pid_t = 0
if AXUIElementGetPid(element, &elementPid) == .success {
eventPid = elementPid // Prefer the element's PID if available
}
let swiftUserInfo = observerCenter.convertCFValueToSwift(userInfo) as? [String: Any]
// Debug logging of the raw callback information
// axDebugLog("AXObserverCallbackWithInfo: PID=\(eventPid), Notification=\(axNotification.rawValue), Element=\(Element(element).briefDescription()), UserInfo=\(String(describing: swiftUserInfo))")
Task {
observerCenter.processNotification(pid: eventPid, notification: axNotification, rawElement: element, nsUserInfo: swiftUserInfo)
}
}
// MARK: - Main Notification Processing (Called by global callbacks)
@MainActor // Ensure this runs on the main actor as handlers are @MainActor
fileprivate func processNotification(pid: pid_t, notification: AXNotification, rawElement: AXUIElement, nsUserInfo: [String: Any]?) {
@ -604,4 +393,4 @@ public class AXObserverCenter {
handler(pid, notification, rawElement, nsUserInfo)
}
}
}
}

View File

@ -0,0 +1,88 @@
// AnyCodable.swift - Type-erased Codable wrapper for mixed-type payloads
import Foundation
// 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
}
}

View File

@ -0,0 +1,115 @@
// CommandEnvelope.swift - Main command envelope structure
import CoreGraphics // For CGPoint
import Foundation
// Main command envelope - REPLACED with definition from axorc.swift for consistency
public struct CommandEnvelope: Codable {
public let commandId: String
public let command: CommandType // Uses CommandType from this file
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debugLogging: Bool
public let locator: Locator? // Locator from this file
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
public let actionName: String? // For performAction
public let actionValue: AnyCodable? // For performAction (AnyCodable from this file)
public let subCommands: [CommandEnvelope]? // For batch command
public let point: CGPoint? // Added for getElementAtPoint
public let pid: Int? // Added for getElementAtPoint (optional specific PID)
// Parameters for 'observe' command
public let notifications: [String]?
public let includeElementDetails: [String]?
public let watchChildren: Bool?
// 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
case command
case application
case attributes
case payload
case debugLogging
case locator
case pathHint
case maxElements
case maxDepth
case outputFormat
case actionName
case actionValue
case subCommands
case point
case pid
// CodingKeys for observe parameters
case notifications
case includeElementDetails
case watchChildren
// CodingKey for new field
case filterCriteria
// Additional CodingKeys
case includeChildrenBrief
case includeChildrenInText
case includeIgnoredElements
}
public init(commandId: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
payload: [String: String]? = nil,
debugLogging: Bool = false,
locator: Locator? = nil,
pathHint: [String]? = nil,
maxElements: Int? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
actionName: String? = nil,
actionValue: AnyCodable? = nil,
subCommands: [CommandEnvelope]? = nil,
point: CGPoint? = nil,
pid: Int? = nil,
notifications: [String]? = nil,
includeElementDetails: [String]? = nil,
watchChildren: Bool? = nil,
filterCriteria: [String: String]? = nil,
includeChildrenBrief: Bool? = nil,
includeChildrenInText: Bool? = nil,
includeIgnoredElements: Bool? = nil
) {
self.commandId = commandId
self.command = command
self.application = application
self.attributes = attributes
self.payload = payload
self.debugLogging = debugLogging
self.locator = locator
self.pathHint = pathHint
self.maxElements = maxElements
self.maxDepth = maxDepth
self.outputFormat = outputFormat
self.actionName = actionName
self.actionValue = actionValue
self.subCommands = subCommands
self.point = point
self.pid = pid
self.notifications = notifications
self.includeElementDetails = includeElementDetails
self.watchChildren = watchChildren
self.filterCriteria = filterCriteria
self.includeChildrenBrief = includeChildrenBrief
self.includeChildrenInText = includeChildrenInText
self.includeIgnoredElements = includeIgnoredElements
}
}

View File

@ -1,660 +1,12 @@
// CommandModels.swift - Contains command-related model structs
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
public let command: CommandType // Uses CommandType from this file
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debugLogging: Bool
public let locator: Locator? // Locator from this file
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
public let actionName: String? // For performAction
public let actionValue: AnyCodable? // For performAction (AnyCodable from this file)
public let subCommands: [CommandEnvelope]? // For batch command
public let point: CGPoint? // Added for getElementAtPoint
public let pid: Int? // Added for getElementAtPoint (optional specific PID)
// Parameters for 'observe' command
public let notifications: [String]?
public let includeElementDetails: [String]?
public let watchChildren: Bool?
// 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
case command
case application
case attributes
case payload
case debugLogging
case locator
case pathHint
case maxElements
case maxDepth
case outputFormat
case actionName
case actionValue
case subCommands
case point
case pid
// CodingKeys for observe parameters
case notifications
case includeElementDetails
case watchChildren
// CodingKey for new field
case filterCriteria
// Additional CodingKeys
case includeChildrenBrief
case includeChildrenInText
case includeIgnoredElements
}
public init(commandId: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
payload: [String: String]? = nil,
debugLogging: Bool = false,
locator: Locator? = nil,
pathHint: [String]? = nil,
maxElements: Int? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
actionName: String? = nil,
actionValue: AnyCodable? = nil,
subCommands: [CommandEnvelope]? = nil,
point: CGPoint? = nil,
pid: Int? = nil,
notifications: [String]? = nil,
includeElementDetails: [String]? = nil,
watchChildren: Bool? = nil,
filterCriteria: [String: String]? = nil,
includeChildrenBrief: Bool? = nil,
includeChildrenInText: Bool? = nil,
includeIgnoredElements: Bool? = nil
) {
self.commandId = commandId
self.command = command
self.application = application
self.attributes = attributes
self.payload = payload
self.debugLogging = debugLogging
self.locator = locator
self.pathHint = pathHint
self.maxElements = maxElements
self.maxDepth = maxDepth
self.outputFormat = outputFormat
self.actionName = actionName
self.actionValue = actionValue
self.subCommands = subCommands
self.point = point
self.pid = pid
self.notifications = notifications
self.includeElementDetails = includeElementDetails
self.watchChildren = watchChildren
self.filterCriteria = filterCriteria
self.includeChildrenBrief = includeChildrenBrief
self.includeChildrenInText = includeChildrenInText
self.includeIgnoredElements = includeIgnoredElements
}
}
// Represents a single criterion for element matching
public struct Criterion: Codable, Sendable {
public let attribute: String
public let value: String
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, 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 = "path_from_root" // Map to JSON key "path_from_root"
case descendantCriteria
case requireAction
case computedNameContains
case debugPathSearch
}
public init(
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,
debugPathSearch: Bool? = false
) {
self.matchAll = matchAll
self.criteria = criteria
self.rootElementPathHint = rootElementPathHint
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
// CommandModels.swift - This file has been split into multiple files for better organization
//
// The types previously in this file have been moved to:
// - ValueFormatOption.swift
// - MatchingTypes.swift (Criterion, PathStep, Locator)
// - CommandTypes.swift (CommandType, OutputFormat)
// - NotificationTypes.swift (AXNotificationName)
// - AnyCodable.swift
// - CommandEnvelope.swift
// - AXCommands.swift (AXCommand and related command structs)
//
// This file is kept empty for backward compatibility.

View File

@ -0,0 +1,32 @@
// CommandTypes.swift - Command type definitions
import Foundation
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.
}

View File

@ -0,0 +1,135 @@
// MatchingTypes.swift - Types for element matching and locating
import Foundation
// Represents a single criterion for element matching
public struct Criterion: Codable, Sendable {
public let attribute: String
public let value: String
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, 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 = "path_from_root" // Map to JSON key "path_from_root"
case descendantCriteria
case requireAction
case computedNameContains
case debugPathSearch
}
public init(
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,
debugPathSearch: Bool? = false
) {
self.matchAll = matchAll
self.criteria = criteria
self.rootElementPathHint = rootElementPathHint
self.descendantCriteria = descendantCriteria
self.requireAction = requireAction
self.computedNameContains = computedNameContains
self.debugPathSearch = debugPathSearch
}
}

View File

@ -0,0 +1,42 @@
// NotificationTypes.swift - AX notification type definitions
import Foundation
// 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"
}

View File

@ -8,7 +8,7 @@ public class NotificationWatcher {
private let target: ObservationTarget
private let notification: AXNotification
private let handler: AXNotificationSubscriptionHandler
private var subscriptionToken: AXObserverCenter.SubscriptionToken?
private var subscriptionToken: SubscriptionToken?
private var isObserving: Bool = false
private enum ObservationTarget {

View File

@ -0,0 +1,54 @@
// ObserverHelpers.swift - Helper functions for AXObserver operations
import ApplicationServices
import Foundation
// MARK: - Helper for userInfo conversion
@MainActor
internal func convertCFValueToSwift(_ cfValue: CFTypeRef?) -> Any? {
guard let cfValue = cfValue else { return nil }
let typeID = CFGetTypeID(cfValue)
switch typeID {
case CFStringGetTypeID():
return cfValue as? String
case CFNumberGetTypeID():
return cfValue as? NSNumber // Could be Int, Double, Bool (via NSNumber bridging)
case CFBooleanGetTypeID():
// Ensure correct conversion for CFBoolean
if CFEqual(cfValue, kCFBooleanTrue) {
return true
} else if CFEqual(cfValue, kCFBooleanFalse) {
return false
}
// Fallback for other CFBoolean representations if any, or if direct Bool bridging works
if let boolVal = cfValue as? Bool {
return boolVal
}
axWarningLog("Could not convert CFBoolean to Bool: \(String(describing: cfValue))")
return nil // Or handle as error
case CFArrayGetTypeID():
// Swift arrays bridge to CFArray, and CFArray can be cast to NSArray / [AnyObject]
if let cfArray = cfValue as? [CFTypeRef] { // or cfValue as? NSArray
return cfArray.compactMap { convertCFValueToSwift($0) }
}
axWarningLog("Failed to convert CFArray from userInfo.")
return cfValue // Return raw CFArray if conversion fails for some reason
case CFDictionaryGetTypeID():
if let cfDict = cfValue as? [CFString: CFTypeRef] { // or cfValue as? NSDictionary
var swiftDict = [String: Any]()
for (key, value) in cfDict {
swiftDict[key as String] = convertCFValueToSwift(value)
}
return swiftDict
}
axWarningLog("Failed to convert nested CFDictionary from userInfo.")
return cfValue // Return raw CFDictionary if conversion fails
case AXUIElementGetTypeID():
return cfValue as! AXUIElement // Should be safe to force unwrap if type matches
// Add other common CF types if necessary, e.g., CFURL, CFDate
default:
axDebugLog("Unhandled CFTypeRef in convertCFValueToSwift: typeID \(typeID). Value: \(cfValue)")
return cfValue // Return raw CFTypeRef if unhandled, caller might know what to do
}
}

View File

@ -0,0 +1,34 @@
// ObserverTypes.swift - Types and structs for AXObserver management
import ApplicationServices
import Foundation
/// Callback type for observer notifications
// public typealias AXObserverHandler = @MainActor (pid_t, AXNotification, AXObserver, AXUIElement, CFDictionary?) -> Void // Old handler
/// New callback type for subscriptions. The AXObserver and AXUIElement might be less relevant to the direct subscriber
/// if the Center abstracts them, or they can be added back if deemed necessary.
public typealias AXNotificationSubscriptionHandler = @MainActor (/*element: Element,*/ pid_t, AXNotification, _ rawElement: AXUIElement, _ nsUserInfo: [String: Any]?) -> Void
/// Key for tracking registered notifications. Can allow nil PID for global observers for a specific notification type.
public struct AXNotificationSubscriptionKey: Hashable {
let pid: pid_t? // Optional to allow for global observers for a specific notification
let notification: AXNotification
}
/// Key and PID pair for tracking registered notifications
public struct AXObserverKeyAndPID: Hashable {
let pid: pid_t
let key: AXNotification
}
/// Observer and PID pair for tracking active observers
public struct AXObserverObjAndPID {
var observer: AXObserver
var pid: pid_t
}
/// Public token for unsubscribing
public struct SubscriptionToken: Hashable {
let id: UUID
}

View File

@ -0,0 +1,11 @@
// ValueFormatOption.swift - Enum for specifying value formatting
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.
}

View File

@ -0,0 +1,132 @@
// AttributeBuilders.swift - Functions for building attribute collections
import ApplicationServices
import CoreGraphics
import Foundation
// MARK: - Attribute Collection Builders
@MainActor
internal func addBasicAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
if let role = element.role() {
attributes[AXAttributeNames.kAXRoleAttribute] = AnyCodable(role)
}
if let subrole = element.subrole() {
attributes[AXAttributeNames.kAXSubroleAttribute] = AnyCodable(subrole)
}
if let title = element.title() {
attributes[AXAttributeNames.kAXTitleAttribute] = AnyCodable(title)
}
if let descriptionText = element.descriptionText() {
attributes[AXAttributeNames.kAXDescriptionAttribute] = AnyCodable(descriptionText)
}
if let value = element.value() {
attributes[AXAttributeNames.kAXValueAttribute] = AnyCodable(value)
}
if let help = element.attribute(Attribute<String>(AXAttributeNames.kAXHelpAttribute)) {
attributes[AXAttributeNames.kAXHelpAttribute] = AnyCodable(help)
}
if let placeholder = element.attribute(Attribute<String>(AXAttributeNames.kAXPlaceholderValueAttribute)) {
attributes[AXAttributeNames.kAXPlaceholderValueAttribute] = AnyCodable(placeholder)
}
}
@MainActor
internal func addStateAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
attributes[AXAttributeNames.kAXEnabledAttribute] = AnyCodable(element.isEnabled())
attributes[AXAttributeNames.kAXFocusedAttribute] = AnyCodable(element.isFocused())
attributes[AXAttributeNames.kAXHiddenAttribute] = AnyCodable(element.isHidden())
attributes[AXMiscConstants.isIgnoredAttributeKey] = AnyCodable(element.isIgnored())
attributes[AXAttributeNames.kAXElementBusyAttribute] = AnyCodable(element.isElementBusy())
}
@MainActor
internal func addGeometryAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
if let position = element.attribute(Attribute<CGPoint>(AXAttributeNames.kAXPositionAttribute)) {
attributes[AXAttributeNames.kAXPositionAttribute] = AnyCodable(NSPointToDictionary(position))
}
if let size = element.attribute(Attribute<CGSize>(AXAttributeNames.kAXSizeAttribute)) {
attributes[AXAttributeNames.kAXSizeAttribute] = AnyCodable(NSSizeToDictionary(size))
}
}
@MainActor
internal func addHierarchyAttributes(to attributes: inout [String: AnyCodable], element: Element, valueFormatOption: ValueFormatOption) async {
if let parent = element.parent() {
attributes[AXAttributeNames.kAXParentAttribute] = AnyCodable(
parent.briefDescription(option: .raw)
)
}
if let children = element.children() {
attributes[AXAttributeNames.kAXChildrenAttribute] = AnyCodable(
children.map { $0.briefDescription(option: .raw) }
)
}
}
@MainActor
internal func addActionAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
var actionsToStore: [String]?
if let currentActions = element.supportedActions(), !currentActions.isEmpty {
actionsToStore = currentActions
} else if let fallbackActions: [String] = element.attribute(
Attribute<[String]>(AXAttributeNames.kAXActionsAttribute)
), !fallbackActions.isEmpty {
actionsToStore = fallbackActions
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Used fallback kAXActionsAttribute for \(element.briefDescription(option: .raw))"))
}
attributes[AXAttributeNames.kAXActionsAttribute] = actionsToStore != nil
? AnyCodable(actionsToStore)
: AnyCodable(nil as [String]?)
if element.isActionSupported(AXActionNames.kAXPressAction) {
attributes["\(AXActionNames.kAXPressAction)_Supported"] = AnyCodable(true)
}
}
@MainActor
internal func addStandardStringAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
let standardAttributes = [
AXAttributeNames.kAXRoleDescriptionAttribute,
AXAttributeNames.kAXValueDescriptionAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
for attrName in standardAttributes {
if attributes[attrName] == nil,
let attrValue: String = element.attribute(Attribute<String>(attrName)) {
attributes[attrName] = AnyCodable(attrValue)
}
}
}
@MainActor
internal func addStoredAttributes(to attributes: inout [String: AnyCodable], element: Element) {
guard let stored = element.attributes else { return }
for (key, val) in stored where attributes[key] == nil {
attributes[key] = val
}
}
@MainActor
internal func addComputedProperties(to attributes: inout [String: AnyCodable], element: Element) async {
if attributes[AXMiscConstants.computedNameAttributeKey] == nil,
let name = element.computedName() {
attributes[AXMiscConstants.computedNameAttributeKey] = AnyCodable(name)
}
if attributes[AXMiscConstants.computedPathAttributeKey] == nil {
attributes[AXMiscConstants.computedPathAttributeKey] = AnyCodable(element.generatePathString())
}
if attributes[AXMiscConstants.isClickableAttributeKey] == nil {
let isButton = element.role() == AXRoleNames.kAXButtonRole
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction)
if isButton || hasPressAction {
attributes[AXMiscConstants.isClickableAttributeKey] = AnyCodable(true)
}
}
}

View File

@ -0,0 +1,157 @@
// AttributeExtractors.swift - Low-level attribute extraction logic
import ApplicationServices
import Foundation
// MARK: - Internal Fetch Logic Helpers
// Approach using direct property access within a switch statement
@MainActor
internal func extractDirectPropertyValue(
for attributeName: String,
from element: Element,
outputFormat: OutputFormat
) -> (value: Any?, handled: Bool) {
var extractedValue: Any?
var handled = true
switch attributeName {
case AXAttributeNames.kAXPathHintAttribute:
extractedValue = element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute))
case AXAttributeNames.kAXRoleAttribute:
extractedValue = element.role()
case AXAttributeNames.kAXSubroleAttribute:
extractedValue = element.subrole()
case AXAttributeNames.kAXTitleAttribute:
extractedValue = element.title()
case AXAttributeNames.kAXDescriptionAttribute:
extractedValue = element.descriptionText() // Renamed
case AXAttributeNames.kAXEnabledAttribute:
let val = element.isEnabled()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXFocusedAttribute:
let val = element.isFocused()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXHiddenAttribute:
let val = element.isHidden()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXMiscConstants.isIgnoredAttributeKey:
let val = element.isIgnored()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val ? "true" : "false"
}
case "PID":
let val = element.pid()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXElementBusyAttribute:
let val = element.isElementBusy()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
default:
handled = false
}
return (extractedValue, handled)
}
@MainActor
internal func determineAttributesToFetch(
requestedAttributes: [String]?,
forMultiDefault: Bool,
targetRole: String?,
element: Element
) -> [String] {
var attributesToFetch = requestedAttributes ?? []
if forMultiDefault {
attributesToFetch = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXTitleAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
if let role = targetRole, role == AXRoleNames.kAXStaticTextRole {
attributesToFetch = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
}
} else if attributesToFetch.isEmpty {
if requestedAttributes == nil || requestedAttributes!.isEmpty {
// If no specific attributes are requested, decide what to do based on context
// This part of the logic for deciding what to fetch if nothing specific is requested
// has been simplified or might be intended to be expanded.
// For now, if forMultiDefault is true, it implies fetching a default set (e.g., for multi-element views)
// otherwise, it might fetch all or a basic set.
// This example assumes if not forMultiDefault, and no specifics, it fetches all available.
if !forMultiDefault {
// Example: Fetch all attribute names if none are specified and not for a multi-default scenario
if let names = element.attributeNames() {
attributesToFetch.append(contentsOf: names)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "determineAttributesToFetch: No specific attributes requested, fetched all \(names.count) available: \(names.joined(separator: ", "))"))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"determineAttributesToFetch: No specific attributes requested and " +
"failed to fetch all available names."
))
}
} else {
// For multi-default, or if the above block doesn't execute,
// it might rely on a predefined default set or do nothing further here,
// letting subsequent logic handle AXorcist.defaultAttributesToFetch if attributesToFetch remains empty.
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"determineAttributesToFetch: No specific attributes requested. Using defaults or context-specific set."
))
}
}
}
return attributesToFetch
}
// Function to get specifically computed attributes for an element
@MainActor
internal func getComputedAttributes(for element: Element) async -> [String: AttributeData] {
var computedAttrs: [String: AttributeData] = [:]
if let name = element.computedName() {
computedAttrs[AXMiscConstants.computedNameAttributeKey] = AttributeData(
value: AnyCodable(name),
source: .computed
)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"getComputedAttributes: Computed name for element " +
"\(element.briefDescription(option: .raw)) is '\(name)'."
))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"getComputedAttributes: Element \(element.briefDescription(option: .raw)) " +
"has no computed name."
))
}
// Placeholder for other future purely computed attributes if needed
// For example, isClickable could also be added here if not handled elsewhere:
// let isButton = (element.role() == AXRoleNames.kAXButtonRole)
// let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction)
// if isButton || hasPressAction {
// computedAttrs[AXMiscConstants.isClickableAttributeKey] = AttributeData(
// value: AnyCodable(true), source: .computed
// )
// }
return computedAttrs
}

View File

@ -0,0 +1,166 @@
// AttributeFormatters.swift - Attribute formatting logic
import ApplicationServices
import CoreGraphics
import Foundation
// Helper for formatting raw CFTypeRef values for .textContent output
@MainActor
internal func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) async -> String {
guard let value = rawValue else { return AXMiscConstants.kAXNotAvailableString }
let typeID = CFGetTypeID(value)
if typeID == CFStringGetTypeID() {
return (value as! String)
} else if typeID == CFAttributedStringGetTypeID() {
return (value as! NSAttributedString).string
} else if typeID == AXValueGetTypeID() {
let axVal = value as! AXValue
return formatAXValue(axVal, option: ValueFormatOption.smart)
} else if typeID == CFNumberGetTypeID() {
return (value as! NSNumber).stringValue
} else if typeID == CFBooleanGetTypeID() {
return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false"
} else {
let typeDesc = CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"formatRawCFValueForTextContent: Encountered unhandled CFTypeID \(typeID) - " +
"\(typeDesc). Returning placeholder."
))
return "<\(typeDesc)>"
}
}
@MainActor
internal func extractAndFormatAttribute(
element: Element,
attributeName: String,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "extractAndFormatAttribute: '\(attributeName)' for element \(element.briefDescription(option: .raw))"))
// Try to extract using known attribute handlers first
if let extractedValue = await extractKnownAttribute(element: element, attributeName: attributeName, outputFormat: outputFormat) {
return AnyCodable(extractedValue)
}
// Fallback to raw attribute value
return await extractRawAttribute(element: element, attributeName: attributeName, outputFormat: outputFormat)
}
@MainActor
private func extractKnownAttribute(element: Element, attributeName: String, outputFormat: OutputFormat) async -> Any? {
switch attributeName {
case AXAttributeNames.kAXPathHintAttribute:
return element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute))
case AXAttributeNames.kAXRoleAttribute:
return element.role()
case AXAttributeNames.kAXSubroleAttribute:
return element.subrole()
case AXAttributeNames.kAXTitleAttribute:
return element.title()
case AXAttributeNames.kAXDescriptionAttribute:
return element.descriptionText()
case AXAttributeNames.kAXEnabledAttribute:
return formatBooleanAttribute(element.isEnabled(), outputFormat: outputFormat)
case AXAttributeNames.kAXFocusedAttribute:
return formatBooleanAttribute(element.isFocused(), outputFormat: outputFormat)
case AXAttributeNames.kAXHiddenAttribute:
return formatBooleanAttribute(element.isHidden(), outputFormat: outputFormat)
case AXMiscConstants.isIgnoredAttributeKey:
let val = element.isIgnored()
return outputFormat == .textContent ? (val ? "true" : "false") : val
case "PID":
return formatOptionalIntAttribute(element.pid(), outputFormat: outputFormat)
case AXAttributeNames.kAXElementBusyAttribute:
return formatBooleanAttribute(element.isElementBusy(), outputFormat: outputFormat)
default:
return nil
}
}
@MainActor
private func formatBooleanAttribute(_ value: Bool?, outputFormat: OutputFormat) -> Any? {
guard let val = value else { return nil }
return outputFormat == .textContent ? val.description : val
}
@MainActor
private func formatOptionalIntAttribute(_ value: Int32?, outputFormat: OutputFormat) -> Any? {
guard let val = value else { return nil }
return outputFormat == .textContent ? val.description : val
}
@MainActor
private func extractRawAttribute(element: Element, attributeName: String, outputFormat: OutputFormat) async -> AnyCodable? {
let rawCFValue = element.rawAttributeValue(named: attributeName)
if outputFormat == .textContent {
let formatted = await formatRawCFValueForTextContent(rawCFValue)
return AnyCodable(formatted)
}
guard let unwrapped = ValueUnwrapper.unwrap(rawCFValue) else {
// Only log if rawCFValue was not nil initially
if rawCFValue != nil {
let cfTypeID = String(describing: CFGetTypeID(rawCFValue!))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"extractAndFormatAttribute: '\(attributeName)' was non-nil CFTypeRef " +
"but unwrapped to nil. CFTypeID: \(cfTypeID)"
))
return AnyCodable("<Raw CFTypeRef: \(cfTypeID)>")
}
return nil
}
return AnyCodable(unwrapped)
}
@MainActor
internal func formatParentAttribute(
_ parent: Element?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let parentElement = parent else { return AnyCodable(nil as String?) }
if outputFormat == .textContent {
return AnyCodable("Element: \(parentElement.role() ?? "?Role")")
} else {
return AnyCodable(parentElement.briefDescription(option: .raw))
}
}
@MainActor
internal func formatChildrenAttribute(
_ children: [Element]?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let actualChildren = children, !actualChildren.isEmpty else {
return AnyCodable(nil as String?)
}
if outputFormat == .textContent {
var childrenSummaries: [String] = []
for childElement in actualChildren {
childrenSummaries.append(childElement.briefDescription(option: .raw))
}
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
} else {
let childrenDescriptions = actualChildren.map { $0.briefDescription(option: .raw) }
return AnyCodable(childrenDescriptions)
}
}
@MainActor
internal func formatFocusedUIElementAttribute(
_ focusedElement: Element?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let element = focusedElement else { return AnyCodable(nil as String?) }
if outputFormat == .textContent {
return AnyCodable("Focused: \(element.role() ?? "?Role") - \(element.title() ?? "?Title")")
} else {
return AnyCodable(element.briefDescription(option: .raw))
}
}

View File

@ -1,182 +1,9 @@
// AttributeHelpers.swift - Contains functions for fetching and formatting element attributes
// AttributeHelpers.swift - Main public API for attribute fetching and formatting
import ApplicationServices // For AXUIElement related types
import CoreGraphics // For potential future use with geometry types from attributes
import ApplicationServices
import CoreGraphics
import Foundation
// ElementDetails struct for AXpector
public struct ElementDetails {
public var title: String?
public var role: String?
public var roleDescription: String?
public var value: Any?
public var help: Any?
public var isIgnored: Bool
public var actions: [String]?
public var isClickable: Bool
public var computedName: String?
public init() {
self.isIgnored = false
self.isClickable = false
}
}
// Enum to specify the source of an attribute
public enum AttributeSource: String, Codable {
case direct // Directly from AXUIElement
case computed // Computed by AXorcist (e.g., path, name heuristic)
case prefetched // From element's stored attributes dictionary
}
// Struct to hold attribute data along with its source
public struct AttributeData: Codable {
public let value: AnyCodable
public let source: AttributeSource
}
// Helper functions to convert CoreGraphics types to dictionaries for JSON serialization
// These are needed because AnyCodable might not handle them directly as dictionaries.
func NSPointToDictionary(_ point: CGPoint) -> [String: CGFloat] {
return ["x": point.x, "y": point.y]
}
func NSSizeToDictionary(_ size: CGSize) -> [String: CGFloat] {
return ["width": size.width, "height": size.height]
}
func NSRectToDictionary(_ rect: CGRect) -> [String: Any] { // Changed to Any for origin/size
return [
"x": rect.origin.x,
"y": rect.origin.y,
"width": rect.size.width,
"height": rect.size.height
]
}
// MARK: - Element Summary Helpers
// Removed getSingleElementSummary as it was unused.
// MARK: - Internal Fetch Logic Helpers
// Approach using direct property access within a switch statement
@MainActor
private func extractDirectPropertyValue(
for attributeName: String,
from element: Element,
outputFormat: OutputFormat
) -> (value: Any?, handled: Bool) {
var extractedValue: Any?
var handled = true
switch attributeName {
case AXAttributeNames.kAXPathHintAttribute:
extractedValue = element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute))
case AXAttributeNames.kAXRoleAttribute:
extractedValue = element.role()
case AXAttributeNames.kAXSubroleAttribute:
extractedValue = element.subrole()
case AXAttributeNames.kAXTitleAttribute:
extractedValue = element.title()
case AXAttributeNames.kAXDescriptionAttribute:
extractedValue = element.descriptionText() // Renamed
case AXAttributeNames.kAXEnabledAttribute:
let val = element.isEnabled()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXFocusedAttribute:
let val = element.isFocused()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXHiddenAttribute:
let val = element.isHidden()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXMiscConstants.isIgnoredAttributeKey:
let val = element.isIgnored()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val ? "true" : "false"
}
case "PID":
let val = element.pid()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
case AXAttributeNames.kAXElementBusyAttribute:
let val = element.isElementBusy()
extractedValue = val
if outputFormat == .textContent {
extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString
}
default:
handled = false
}
return (extractedValue, handled)
}
@MainActor
private func determineAttributesToFetch(
requestedAttributes: [String]?,
forMultiDefault: Bool,
targetRole: String?,
element: Element
) -> [String] {
var attributesToFetch = requestedAttributes ?? []
if forMultiDefault {
attributesToFetch = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXTitleAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
if let role = targetRole, role == AXRoleNames.kAXStaticTextRole {
attributesToFetch = [
AXAttributeNames.kAXRoleAttribute,
AXAttributeNames.kAXValueAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
}
} else if attributesToFetch.isEmpty {
if requestedAttributes == nil || requestedAttributes!.isEmpty {
// If no specific attributes are requested, decide what to do based on context
// This part of the logic for deciding what to fetch if nothing specific is requested
// has been simplified or might be intended to be expanded.
// For now, if forMultiDefault is true, it implies fetching a default set (e.g., for multi-element views)
// otherwise, it might fetch all or a basic set.
// This example assumes if not forMultiDefault, and no specifics, it fetches all available.
if !forMultiDefault {
// Example: Fetch all attribute names if none are specified and not for a multi-default scenario
if let names = element.attributeNames() {
attributesToFetch.append(contentsOf: names)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "determineAttributesToFetch: No specific attributes requested, fetched all \(names.count) available: \(names.joined(separator: ", "))"))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"determineAttributesToFetch: No specific attributes requested and " +
"failed to fetch all available names."
))
}
} else {
// For multi-default, or if the above block doesn't execute,
// it might rely on a predefined default set or do nothing further here,
// letting subsequent logic handle AXorcist.defaultAttributesToFetch if attributesToFetch remains empty.
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"determineAttributesToFetch: No specific attributes requested. Using defaults or context-specific set."
))
}
}
}
return attributesToFetch
}
// MARK: - Public Attribute Getters
@MainActor
@ -301,154 +128,6 @@ public func getAllElementDataForAXpector(
return (attributes, elementDetails)
}
// Function to get specifically computed attributes for an element
@MainActor
internal func getComputedAttributes(for element: Element) async -> [String: AttributeData] {
var computedAttrs: [String: AttributeData] = [:]
if let name = element.computedName() {
computedAttrs[AXMiscConstants.computedNameAttributeKey] = AttributeData(
value: AnyCodable(name),
source: .computed
)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"getComputedAttributes: Computed name for element " +
"\(element.briefDescription(option: .raw)) is '\(name)'."
))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"getComputedAttributes: Element \(element.briefDescription(option: .raw)) " +
"has no computed name."
))
}
// Placeholder for other future purely computed attributes if needed
// For example, isClickable could also be added here if not handled elsewhere:
// let isButton = (element.role() == AXRoleNames.kAXButtonRole)
// let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction)
// if isButton || hasPressAction {
// computedAttrs[AXMiscConstants.isClickableAttributeKey] = AttributeData(
// value: AnyCodable(true), source: .computed
// )
// }
return computedAttrs
}
// Helper for formatting raw CFTypeRef values for .textContent output
@MainActor
internal func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) async -> String {
guard let value = rawValue else { return AXMiscConstants.kAXNotAvailableString }
let typeID = CFGetTypeID(value)
if typeID == CFStringGetTypeID() {
return (value as! String)
} else if typeID == CFAttributedStringGetTypeID() {
return (value as! NSAttributedString).string
} else if typeID == AXValueGetTypeID() {
let axVal = value as! AXValue
return formatAXValue(axVal, option: ValueFormatOption.smart)
} else if typeID == CFNumberGetTypeID() {
return (value as! NSNumber).stringValue
} else if typeID == CFBooleanGetTypeID() {
return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false"
} else {
let typeDesc = CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"formatRawCFValueForTextContent: Encountered unhandled CFTypeID \(typeID) - " +
"\(typeDesc). Returning placeholder."
))
return "<\(typeDesc)>"
}
}
// formatAXValue is now defined in Values/AXValueSpecificFormatter.swift
@MainActor
internal func extractAndFormatAttribute(
element: Element,
attributeName: String,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "extractAndFormatAttribute: '\(attributeName)' for element \(element.briefDescription(option: .raw))"))
// Try to extract using known attribute handlers first
if let extractedValue = await extractKnownAttribute(element: element, attributeName: attributeName, outputFormat: outputFormat) {
return AnyCodable(extractedValue)
}
// Fallback to raw attribute value
return await extractRawAttribute(element: element, attributeName: attributeName, outputFormat: outputFormat)
}
@MainActor
private func extractKnownAttribute(element: Element, attributeName: String, outputFormat: OutputFormat) async -> Any? {
switch attributeName {
case AXAttributeNames.kAXPathHintAttribute:
return element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute))
case AXAttributeNames.kAXRoleAttribute:
return element.role()
case AXAttributeNames.kAXSubroleAttribute:
return element.subrole()
case AXAttributeNames.kAXTitleAttribute:
return element.title()
case AXAttributeNames.kAXDescriptionAttribute:
return element.descriptionText()
case AXAttributeNames.kAXEnabledAttribute:
return formatBooleanAttribute(element.isEnabled(), outputFormat: outputFormat)
case AXAttributeNames.kAXFocusedAttribute:
return formatBooleanAttribute(element.isFocused(), outputFormat: outputFormat)
case AXAttributeNames.kAXHiddenAttribute:
return formatBooleanAttribute(element.isHidden(), outputFormat: outputFormat)
case AXMiscConstants.isIgnoredAttributeKey:
let val = element.isIgnored()
return outputFormat == .textContent ? (val ? "true" : "false") : val
case "PID":
return formatOptionalIntAttribute(element.pid(), outputFormat: outputFormat)
case AXAttributeNames.kAXElementBusyAttribute:
return formatBooleanAttribute(element.isElementBusy(), outputFormat: outputFormat)
default:
return nil
}
}
@MainActor
private func formatBooleanAttribute(_ value: Bool?, outputFormat: OutputFormat) -> Any? {
guard let val = value else { return nil }
return outputFormat == .textContent ? val.description : val
}
@MainActor
private func formatOptionalIntAttribute(_ value: Int32?, outputFormat: OutputFormat) -> Any? {
guard let val = value else { return nil }
return outputFormat == .textContent ? val.description : val
}
@MainActor
private func extractRawAttribute(element: Element, attributeName: String, outputFormat: OutputFormat) async -> AnyCodable? {
let rawCFValue = element.rawAttributeValue(named: attributeName)
if outputFormat == .textContent {
let formatted = await formatRawCFValueForTextContent(rawCFValue)
return AnyCodable(formatted)
}
guard let unwrapped = ValueUnwrapper.unwrap(rawCFValue) else {
// Only log if rawCFValue was not nil initially
if rawCFValue != nil {
let cfTypeID = String(describing: CFGetTypeID(rawCFValue!))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"extractAndFormatAttribute: '\(attributeName)' was non-nil CFTypeRef " +
"but unwrapped to nil. CFTypeID: \(cfTypeID)"
))
return AnyCodable("<Raw CFTypeRef: \(cfTypeID)>")
}
return nil
}
return AnyCodable(unwrapped)
}
@MainActor
public func getElementFullDescription(
element: Element,
@ -483,218 +162,4 @@ public func getElementFullDescription(
"\(element.briefDescription(option: .raw)). Returning \(attributes.count) attributes."
))
return (attributes, [])
}
@MainActor
private func addBasicAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
if let role = element.role() {
attributes[AXAttributeNames.kAXRoleAttribute] = AnyCodable(role)
}
if let subrole = element.subrole() {
attributes[AXAttributeNames.kAXSubroleAttribute] = AnyCodable(subrole)
}
if let title = element.title() {
attributes[AXAttributeNames.kAXTitleAttribute] = AnyCodable(title)
}
if let descriptionText = element.descriptionText() {
attributes[AXAttributeNames.kAXDescriptionAttribute] = AnyCodable(descriptionText)
}
if let value = element.value() {
attributes[AXAttributeNames.kAXValueAttribute] = AnyCodable(value)
}
if let help = element.attribute(Attribute<String>(AXAttributeNames.kAXHelpAttribute)) {
attributes[AXAttributeNames.kAXHelpAttribute] = AnyCodable(help)
}
if let placeholder = element.attribute(Attribute<String>(AXAttributeNames.kAXPlaceholderValueAttribute)) {
attributes[AXAttributeNames.kAXPlaceholderValueAttribute] = AnyCodable(placeholder)
}
}
@MainActor
private func addStateAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
attributes[AXAttributeNames.kAXEnabledAttribute] = AnyCodable(element.isEnabled())
attributes[AXAttributeNames.kAXFocusedAttribute] = AnyCodable(element.isFocused())
attributes[AXAttributeNames.kAXHiddenAttribute] = AnyCodable(element.isHidden())
attributes[AXMiscConstants.isIgnoredAttributeKey] = AnyCodable(element.isIgnored())
attributes[AXAttributeNames.kAXElementBusyAttribute] = AnyCodable(element.isElementBusy())
}
@MainActor
private func addGeometryAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
if let position = element.attribute(Attribute<CGPoint>(AXAttributeNames.kAXPositionAttribute)) {
attributes[AXAttributeNames.kAXPositionAttribute] = AnyCodable(NSPointToDictionary(position))
}
if let size = element.attribute(Attribute<CGSize>(AXAttributeNames.kAXSizeAttribute)) {
attributes[AXAttributeNames.kAXSizeAttribute] = AnyCodable(NSSizeToDictionary(size))
}
}
@MainActor
private func addHierarchyAttributes(to attributes: inout [String: AnyCodable], element: Element, valueFormatOption: ValueFormatOption) async {
if let parent = element.parent() {
attributes[AXAttributeNames.kAXParentAttribute] = AnyCodable(
parent.briefDescription(option: .raw)
)
}
if let children = element.children() {
attributes[AXAttributeNames.kAXChildrenAttribute] = AnyCodable(
children.map { $0.briefDescription(option: .raw) }
)
}
}
@MainActor
private func addActionAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
var actionsToStore: [String]?
if let currentActions = element.supportedActions(), !currentActions.isEmpty {
actionsToStore = currentActions
} else if let fallbackActions: [String] = element.attribute(
Attribute<[String]>(AXAttributeNames.kAXActionsAttribute)
), !fallbackActions.isEmpty {
actionsToStore = fallbackActions
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Used fallback kAXActionsAttribute for \(element.briefDescription(option: .raw))"))
}
attributes[AXAttributeNames.kAXActionsAttribute] = actionsToStore != nil
? AnyCodable(actionsToStore)
: AnyCodable(nil as [String]?)
if element.isActionSupported(AXActionNames.kAXPressAction) {
attributes["\(AXActionNames.kAXPressAction)_Supported"] = AnyCodable(true)
}
}
@MainActor
private func addStandardStringAttributes(to attributes: inout [String: AnyCodable], element: Element) async {
let standardAttributes = [
AXAttributeNames.kAXRoleDescriptionAttribute,
AXAttributeNames.kAXValueDescriptionAttribute,
AXAttributeNames.kAXIdentifierAttribute
]
for attrName in standardAttributes {
if attributes[attrName] == nil,
let attrValue: String = element.attribute(Attribute<String>(attrName)) {
attributes[attrName] = AnyCodable(attrValue)
}
}
}
@MainActor
private func addStoredAttributes(to attributes: inout [String: AnyCodable], element: Element) {
guard let stored = element.attributes else { return }
for (key, val) in stored where attributes[key] == nil {
attributes[key] = val
}
}
@MainActor
private func addComputedProperties(to attributes: inout [String: AnyCodable], element: Element) async {
if attributes[AXMiscConstants.computedNameAttributeKey] == nil,
let name = element.computedName() {
attributes[AXMiscConstants.computedNameAttributeKey] = AnyCodable(name)
}
if attributes[AXMiscConstants.computedPathAttributeKey] == nil {
attributes[AXMiscConstants.computedPathAttributeKey] = AnyCodable(element.generatePathString())
}
if attributes[AXMiscConstants.isClickableAttributeKey] == nil {
let isButton = element.role() == AXRoleNames.kAXButtonRole
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction)
if isButton || hasPressAction {
attributes[AXMiscConstants.isClickableAttributeKey] = AnyCodable(true)
}
}
}
@MainActor
private func formatParentAttribute(
_ parent: Element?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let parentElement = parent else { return AnyCodable(nil as String?) }
if outputFormat == .textContent {
return AnyCodable("Element: \(parentElement.role() ?? "?Role")")
} else {
return AnyCodable(parentElement.briefDescription(option: .raw))
}
}
@MainActor
private func formatChildrenAttribute(
_ children: [Element]?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let actualChildren = children, !actualChildren.isEmpty else {
return AnyCodable(nil as String?)
}
if outputFormat == .textContent {
var childrenSummaries: [String] = []
for childElement in actualChildren {
childrenSummaries.append(childElement.briefDescription(option: .raw))
}
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
} else {
let childrenDescriptions = actualChildren.map { $0.briefDescription(option: .raw) }
return AnyCodable(childrenDescriptions)
}
}
@MainActor
private func formatFocusedUIElementAttribute(
_ focusedElement: Element?,
outputFormat: OutputFormat,
valueFormatOption: ValueFormatOption
) async -> AnyCodable {
guard let element = focusedElement else { return AnyCodable(nil as String?) }
if outputFormat == .textContent {
return AnyCodable("Focused: \(element.role() ?? "?Role") - \(element.title() ?? "?Title")")
} else {
return AnyCodable(element.briefDescription(option: .raw))
}
}
// formatValue is likely not needed anymore if ValueUnwrapper is robust and
// extractAndFormatAttribute handles types correctly.
// Keeping it commented out for now, can be removed if confirmed.
/*
@MainActor
func formatValue(_ value: Any?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption) -> AnyCodable? {
guard let val = value else { return nil }
if outputFormat == .textContent {
if let strVal = val as? String { return AnyCodable(strVal) }
if let attrStrVal = val as? NSAttributedString { return AnyCodable(attrStrVal.string) }
if let boolVal = val as? Bool { return AnyCodable(boolVal.description) }
if let numVal = val as? NSNumber { return AnyCodable(numVal.stringValue) }
// For other complex types, a generic description
return AnyCodable("<".appending(String(describing: type(of: val))).appending(">"))
}
// For JSON or other structured output, try to preserve type or use AnyCodable
if let axVal = val as? AXValue { // AXValue might not be directly Codable
return AnyCodable(formatAXValue(axVal, option: valueFormatOption))
} else if let elementVal = val as? Element { // Element might not be directly Codable in all contexts
return AnyCodable(elementVal.briefDescription(option: valueFormatOption))
} else if let arrayVal = val as? [Any?] {
return AnyCodable(
arrayVal.map {
formatValue($0, outputFormat: outputFormat, valueFormatOption: valueFormatOption)?.value
}
) // Recursively format
} else if let dictVal = val as? [String: Any?] {
return AnyCodable(
dictVal.mapValues {
formatValue($0, outputFormat: outputFormat, valueFormatOption: valueFormatOption)?.value
}
)
}
return AnyCodable(val) // Fallback to AnyCodable directly
}
*/
}

View File

@ -0,0 +1,34 @@
// AttributeTypes.swift - Core types for attribute handling
import Foundation
// ElementDetails struct for AXpector
public struct ElementDetails {
public var title: String?
public var role: String?
public var roleDescription: String?
public var value: Any?
public var help: Any?
public var isIgnored: Bool
public var actions: [String]?
public var isClickable: Bool
public var computedName: String?
public init() {
self.isIgnored = false
self.isClickable = false
}
}
// Enum to specify the source of an attribute
public enum AttributeSource: String, Codable {
case direct // Directly from AXUIElement
case computed // Computed by AXorcist (e.g., path, name heuristic)
case prefetched // From element's stored attributes dictionary
}
// Struct to hold attribute data along with its source
public struct AttributeData: Codable {
public let value: AnyCodable
public let source: AttributeSource
}

View File

@ -0,0 +1,23 @@
// GeometryHelpers.swift - Helper functions for geometry type conversions
import CoreGraphics
import Foundation
// Helper functions to convert CoreGraphics types to dictionaries for JSON serialization
// These are needed because AnyCodable might not handle them directly as dictionaries.
func NSPointToDictionary(_ point: CGPoint) -> [String: CGFloat] {
return ["x": point.x, "y": point.y]
}
func NSSizeToDictionary(_ size: CGSize) -> [String: CGFloat] {
return ["width": size.width, "height": size.height]
}
func NSRectToDictionary(_ rect: CGRect) -> [String: Any] { // Changed to Any for origin/size
return [
"x": rect.origin.x,
"y": rect.origin.y,
"width": rect.size.width,
"height": rect.size.height
]
}

View File

@ -0,0 +1,86 @@
// PathNavigationCore.swift - Core path navigation functions
import ApplicationServices
import Foundation
// MARK: - Core Navigation Functions
@MainActor
internal func navigateToElement(
from startElement: Element,
pathHint: [String],
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) -> Element? {
var currentElement = startElement
var currentPathSegmentForLog = ""
for (index, pathComponentString) in pathHint.enumerated() {
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
if index == 0 && pathComponentString.lowercased() == "application" {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Path component 'application' encountered. Using current element (app root) as context for next component."))
continue
}
if index >= maxDepth {
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)'"))
return nil
}
if let nextElement = processPathComponent(
currentElement: currentElement,
pathComponentString: pathComponentString,
criteriaToMatch: criteriaToMatch,
currentPathSegmentForLog: currentPathSegmentForLog
) {
currentElement = nextElement
} else {
return nil
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation successful. Final element: \(currentElement.briefDescription(option: ValueFormatOption.smart))"))
return currentElement
}
@MainActor
internal func processPathComponent(
currentElement: Element,
pathComponentString: String,
criteriaToMatch: [String: String],
currentPathSegmentForLog: String
) -> Element? {
let currentElementDescForLog = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Processing path component '\(pathComponentString)' at element [\(currentElementDescForLog)]. Path: \(currentPathSegmentForLog)"))
if let matchingChild = findMatchingChild(parentElement: currentElement, criteriaToMatch: criteriaToMatch, pathComponentForLog: pathComponentString) {
return matchingChild
}
if elementMatchesAllCriteria(currentElement, criteria: criteriaToMatch, forPathComponent: pathComponentString) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Path component '\(pathComponentString)' matches current element [\(currentElementDescForLog)]."))
return currentElement
}
logNoMatchFound(currentElement: currentElement, pathComponentString: pathComponentString, criteriaToMatch: criteriaToMatch, currentPathSegmentForLog: currentPathSegmentForLog)
return nil
}
@MainActor
internal func getChildrenFromElement(_ element: Element) -> [Element]? {
guard let children = element.children() else {
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: ValueFormatOption.smart))] has zero children (returned empty array for .children())."))
}
return children
}

View File

@ -0,0 +1,190 @@
// PathNavigationJSON.swift - JSON path hint navigation
import ApplicationServices
import Foundation
import Logging
// Define logger for this file
private let logger = Logger(label: "AXorcist.PathNavigationJSON")
// MARK: - JSON PathHint Navigation
@MainActor
internal func navigateToElementByJSONPathHint(
from startElement: Element,
jsonPathHint: [JSONPathHintComponent],
overallMaxDepth: Int = AXMiscConstants.defaultMaxDepthSearch,
initialPathSegmentForLog: String = "Application"
) -> Element? {
var currentElement = startElement
var currentPathSegmentForLog = initialPathSegmentForLog
for (index, pathComponent) in jsonPathHint.enumerated() {
let componentLogString = pathComponent.descriptionForLog()
currentPathSegmentForLog += " -> " + componentLogString
if pathComponent.attribute.lowercased() == "application" {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: JSON path component \(index) is 'application'. Using current element (app root) as context for next component."))
continue
}
if index >= overallMaxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Navigation aborted: JSON path hint index \(index) reached overallMaxDepth \(overallMaxDepth). Path so far: \(currentPathSegmentForLog)"))
return nil
}
if let nextElement = processJSONPathComponent(
currentElement: currentElement,
pathComponent: pathComponent,
currentPathSegmentForLog: currentPathSegmentForLog,
componentLogString: componentLogString
) {
currentElement = nextElement
} else {
return nil
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JPHN: Navigation successful. Final element: [\(currentElement.briefDescription(option: ValueFormatOption.smart))]. Full path: \(currentPathSegmentForLog)"))
return currentElement
}
@MainActor
private func processJSONPathComponent(
currentElement: Element,
pathComponent: JSONPathHintComponent,
currentPathSegmentForLog: String,
componentLogString: String
) -> Element? {
let currentElementDescForLog = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Processing JSON path component '\(componentLogString)' at element [\(currentElementDescForLog)]. Path: \(currentPathSegmentForLog)"))
let criteriaToMatch = convertJSONPathComponentToCriteria(pathComponent)
let actualMatchType = pathComponent.matchType ?? .exact
let actualMaxDepthForSearch = pathComponent.depth ?? 1
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Converted JSON component to criteria: \(criteriaToMatch). MatchType: \(actualMatchType.rawValue), MaxDepthForSearch: \(actualMaxDepthForSearch)"))
if actualMaxDepthForSearch > 1 {
if let deepMatch = findMatchRecursively(
in: currentElement,
criteria: criteriaToMatch,
matchType: actualMatchType,
maxDepth: actualMaxDepthForSearch,
pathComponentForLog: componentLogString
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JPHN: Deep match found for component '\(componentLogString)': [\(deepMatch.briefDescription(option: ValueFormatOption.smart))]"))
return deepMatch
}
} else {
if let directChild = findMatchingChildJSON(
parentElement: currentElement,
criteriaToMatch: criteriaToMatch,
matchType: actualMatchType,
pathComponentForLog: componentLogString
) {
return directChild
}
if elementMatchesAllCriteriaJSON(currentElement, criteria: criteriaToMatch, matchType: actualMatchType, forPathComponent: componentLogString) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: JSON path component '\(componentLogString)' matches current element [\(currentElementDescForLog)]."))
return currentElement
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/JPHN: JSON path component '\(componentLogString)' with criteria \(criteriaToMatch) did not match any child or current element [\(currentElementDescForLog)]. Path so far: \(currentPathSegmentForLog). Search depth was \(actualMaxDepthForSearch)."))
return nil
}
@MainActor
private func convertJSONPathComponentToCriteria(_ component: JSONPathHintComponent) -> [String: String] {
// Use the component's simpleCriteria property which handles the attribute mapping
return component.simpleCriteria ?? [:]
}
@MainActor
private func findMatchingChildJSON(
parentElement: Element,
criteriaToMatch: [String: String],
matchType: JSONPathHintComponent.MatchType,
pathComponentForLog: String
) -> Element? {
guard let children = getChildrenFromElement(parentElement) else {
return nil
}
for (childIndex, child) in children.enumerated() {
if elementMatchesAllCriteriaJSON(child, criteria: criteriaToMatch, matchType: matchType, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMCJ: Found matching child at index \(childIndex) for JSON component [\(pathComponentForLog)]: [\(child.briefDescription(option: ValueFormatOption.smart))]."))
return child
}
}
return nil
}
@MainActor
private func elementMatchesAllCriteriaJSON(
_ element: Element,
criteria: [String: String],
matchType: JSONPathHintComponent.MatchType,
forPathComponent pathComponentForLog: String
) -> Bool {
if criteria.isEmpty {
return true
}
for (key, expectedValue) in criteria {
let criterionDidMatch = matchSingleCriterion(
element: element,
key: key,
expectedValue: expectedValue,
matchType: matchType,
elementDescriptionForLog: element.briefDescription(option: ValueFormatOption.smart)
)
if !criterionDidMatch {
return false
}
}
return true
}
@MainActor
private func findMatchRecursively(
in rootElement: Element,
criteria: [String: String],
matchType: JSONPathHintComponent.MatchType,
maxDepth: Int,
pathComponentForLog: String
) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMR: Starting recursive search for component '\(pathComponentForLog)' with maxDepth \(maxDepth) from [\(rootElement.briefDescription(option: ValueFormatOption.smart))]"))
var queue: [(element: Element, depth: Int)] = [(rootElement, 0)]
var visited = Set<Element>()
while !queue.isEmpty {
let (currentElement, currentDepth) = queue.removeFirst()
if visited.contains(currentElement) {
continue
}
visited.insert(currentElement)
if elementMatchesAllCriteriaJSON(currentElement, criteria: criteria, matchType: matchType, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMR: Found match at depth \(currentDepth): [\(currentElement.briefDescription(option: ValueFormatOption.smart))]"))
return currentElement
}
if currentDepth < maxDepth {
if let children = currentElement.children() {
for child in children {
queue.append((child, currentDepth + 1))
}
}
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMR: No match found in recursive search for component '\(pathComponentForLog)'"))
return nil
}

View File

@ -0,0 +1,76 @@
// PathNavigationMatching.swift - Element matching functions for path navigation
import ApplicationServices
import Foundation
// MARK: - Element Matching
// New helper to check if an element matches all given criteria
@MainActor
internal func elementMatchesAllCriteria(
_ element: Element,
criteria: [String: String],
forPathComponent pathComponentForLog: String // For logging
) -> 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: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED (empty criteria) for component [\(pathComponentForLog)]."))
return true
}
for (key, expectedValue) in criteria {
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)]."))
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: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED ALL criteria for component [\(pathComponentForLog)]."))
return true
}
@MainActor
internal func findMatchingChild(
parentElement: Element,
criteriaToMatch: [String: String],
pathComponentForLog: String
) -> Element? {
guard let children = getChildrenFromElement(parentElement) else {
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Searching for matching child among \(children.count) children of [\(parentElement.briefDescription(option: ValueFormatOption.smart))] for component [\(pathComponentForLog)]."))
for (childIndex, child) in children.enumerated() {
if elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC: Found matching child at index \(childIndex) for component [\(pathComponentForLog)]: [\(child.briefDescription(option: ValueFormatOption.smart))]."))
return child
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: No matching child found for component [\(pathComponentForLog)] among \(children.count) children."))
return nil
}
@MainActor
internal func logNoMatchFound(
currentElement: Element,
pathComponentString: String,
criteriaToMatch: [String: String],
currentPathSegmentForLog: String
) {
let currentElementDescForLog = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Path component '\(pathComponentString)' with criteria \(criteriaToMatch) did not match any child or current element [\(currentElementDescForLog)]. Path so far: \(currentPathSegmentForLog)"))
}

View File

@ -0,0 +1,132 @@
// PathNavigationUtilities.swift - Utility functions for path navigation
import AppKit
import ApplicationServices
import Foundation
import Logging
// Define logger for this file
private let logger = Logger(label: "AXorcist.PathNavigationUtilities")
// MARK: - Application Element Utilities
@MainActor
public func getApplicationElement(for bundleIdentifier: String) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/AppEl: Attempting to get application element for bundle identifier '\(bundleIdentifier)'."))
guard let runningApp = NSWorkspace.shared.runningApplications.first(where: {
$0.bundleIdentifier == bundleIdentifier
}) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/AppEl: Could not find running application with bundle identifier '\(bundleIdentifier)'."))
return nil
}
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
}
@MainActor
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 {
bundleIdMessagePart = ""
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for PID \(processId)\(bundleIdMessagePart): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
return appElement
}
// MARK: - Element from Path (High-Level)
@MainActor
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
}
}
// MARK: - Path-based Search
@MainActor
func findDescendantAtPath(
currentRoot: Element,
pathComponents: [PathStep],
maxDepth: Int,
debugSearch: Bool
) -> Element? {
var currentElement = currentRoot
logger.debug("PathNav/findDescendantAtPath: Starting path navigation. Initial root: \(currentElement.briefDescription(option: .smart)). Path components: \(pathComponents.count)")
for (pathComponentIndex, component) in pathComponents.enumerated() {
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
)
// 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
}
logger.debug("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Found \(childrenToSearch.count) children to search.")
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
}
}
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

@ -1,510 +1,12 @@
// PathNavigator.swift - Contains logic for navigating element hierarchies using path hints
// PathNavigator.swift - Main entry point for path navigation functionality
//
// This file has been split into several focused modules:
// - PathNavigationCore.swift - Core navigation functions
// - PathNavigationMatching.swift - Element matching logic
// - PathNavigationJSON.swift - JSON path hint navigation
// - PathNavigationUtilities.swift - Utility functions and high-level APIs
//
// All public functions remain accessible through importing this module.
import ApplicationServices
import Foundation
import AppKit // Added for NSRunningApplication
import Logging // Import Logging
// 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
private func elementMatchesAllCriteria(
_ element: Element,
criteria: [String: String],
forPathComponent pathComponentForLog: String // For logging
) -> 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: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED (empty criteria) for component [\(pathComponentForLog)]."))
return true
}
for (key, expectedValue) in criteria {
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)]."))
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: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED ALL criteria for component [\(pathComponentForLog)]."))
return true
}
// Updated navigateToElement to prioritize children
@MainActor
internal func navigateToElement(
from startElement: Element,
pathHint: [String],
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) -> Element? {
var currentElement = startElement
var currentPathSegmentForLog = ""
for (index, pathComponentString) in pathHint.enumerated() {
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
if index == 0 && pathComponentString.lowercased() == "application" {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Path component 'application' encountered. Using current element (app root) as context for next component."))
continue
}
if index >= maxDepth {
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)'"))
return nil
}
if let nextElement = processPathComponent(
currentElement: currentElement,
pathComponentString: pathComponentString,
criteriaToMatch: criteriaToMatch,
currentPathSegmentForLog: currentPathSegmentForLog
) {
currentElement = nextElement
} else {
return nil
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Navigation successful. Final element: \(currentElement.briefDescription(option: ValueFormatOption.smart))"))
return currentElement
}
@MainActor
private func processPathComponent(
currentElement: Element,
pathComponentString: String,
criteriaToMatch: [String: String],
currentPathSegmentForLog: String
) -> 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."))
stepCounter += 1
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."))
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."))
stepCounter += 1
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."))
return matchedChild
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/PPC: Step \(stepCounter). findMatchingChild returned nil. Before elementMatchesAllCriteria."))
stepCounter += 1
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."))
stepCounter += 1
logNoMatchFound(
briefDesc: briefDesc,
pathComponentString: pathComponentString,
currentPathSegmentForLog: currentPathSegmentForLog
)
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)"))
}
@MainActor
private func logNoMatchFound(
briefDesc: String,
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"))
}
@MainActor
private func findMatchingChild(
currentElement: Element,
criteriaToMatch: [String: String],
pathComponentForLog: String
) -> 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 {
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 \(parentElementDesc) has \(children.count) children. Iterating..."))
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: 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) -> [Element]? {
guard let children = element.children() else {
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: ValueFormatOption.smart))] has zero children (returned empty array for .children())."))
}
return children
}
// MARK: - Index-based Navigation (If still needed, would need careful review)
// The PathUtils.parseRichPathComponent currently does not produce index-based hints.
// If "@index" style hints are required, parseRichPathComponent and the matching logic
// would need to be extended. For now, focusing on attribute-based matching.
/*
// Example of how index-based logic might be integrated if parseRichPathComponent supported it
// (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 = await getChildrenFromElement(currentElement), index >= 0, index < children.count { // Added await
let indexedChild = children[index]
await axDebugLog(\"Path component \'\\(pathComponentString)\' resolved to child at index \\(index): \\(await indexedChild.briefDescription())\") // Added await
return indexedChild
} else {
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
}
}
*/
// MARK: - Deprecated/Replaced original path navigation helpers
// The following functions were part of an older path navigation system or single-attribute matching
// and are now replaced by the richer criteria-based matching using elementMatchesAllCriteria.
// They are kept here commented out for reference during transition and can be removed later.
/*
@MainActor
internal func original_currentElementMatchesPathComponent( // Marked as original
_ element: Element,
attributeName: String,
expectedValue: String
) async -> Bool { // Made async
if attributeName.isEmpty {
await axWarningLog(\"original_currentElementMatchesPathComponent: attributeName is empty.\") // Added await
return false
}
// ... (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
}
}
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 findDescendantMatchingCriteria(
startElement: Element,
criteria: [Criterion],
maxDepth: Int,
stopAtFirstMatch: Bool,
pathComponentForLog: String
) -> Element? {
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
}
guard let children = startElement.children() else {
return nil
}
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
}
// MARK: - Application Root Element Navigation
@MainActor
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
}
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
}
@MainActor
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 {
bundleIdMessagePart = ""
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for PID \(processId)\(bundleIdMessagePart): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
return appElement
}
// MARK: - Element from Path (High-Level)
@MainActor
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,
debugSearch: Bool
) -> Element? {
var currentElement = currentRoot
logger.debug("PathNav/findDescendantAtPath: Starting path navigation. Initial root: \\(currentElement.briefDescription(option: .smart)). Path components: \\(pathComponents.count)")
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
)
// 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
}
logger.debug("PathNav/findDescendantAtPath: [Component \\(pathComponentIndex + 1)] Found \\(childrenToSearch.count) children to search.")
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
}
}
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
}
import Foundation

View File

@ -0,0 +1,67 @@
// CommandExecutionFunctions.swift - Individual command execution functions
import AXorcist
import Foundation
// MARK: - Command Execution Functions (now call AXorcist.runCommand)
@MainActor
internal func executeQuery(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axQueryCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Query to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Query")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axQueryCommand))
return HandlerResponse(from: axResponse)
}
@MainActor
internal func executeGetFocusedElement(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axGetFocusedCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert GetFocusedElement to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetFocusedElement")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axGetFocusedCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
internal func executeGetAttributes(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axGetAttrsCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert GetAttributes to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetAttributes")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axGetAttrsCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
internal func executeDescribeElement(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axDescribeCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert DescribeElement to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for DescribeElement")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axDescribeCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
internal func executeExtractText(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axExtractCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert ExtractText to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for ExtractText")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axExtractCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
internal func executePerformAction(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axPerformCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert PerformAction to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for PerformAction")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axPerformCmd))
return HandlerResponse(from: axResponse)
}

View File

@ -0,0 +1,158 @@
// CommandExecutor.swift - Main command executor that coordinates command processing
import AppKit // For NSRunningApplication
import AXorcist
import Foundation
@MainActor
struct CommandExecutor {
@MainActor
static func execute(
command: CommandEnvelope,
axorcist: AXorcist,
debugCLI: Bool // This is from the --debug CLI flag
) -> String {
// The main AXORCCommand.run() now sets the global logging based on --debug.
// CommandExecutor.setupLogging can adjust detail level if command.debugLogging is true.
let previousDetailLevel = setupDetailLevelForCommand(commandDebugLogging: command.debugLogging, cliDebug: debugCLI)
defer {
// Restore only the detail level if it was changed.
if let prevLevel = previousDetailLevel {
GlobalAXLogger.shared.detailLevel = prevLevel
}
}
axDebugLog("Executing command: \(command.command) (ID: \(command.commandId)), cmdDebug: \(command.debugLogging), cliDebug: \(debugCLI)")
let responseString = processCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
return responseString
}
// Simplified to only adjust detail level based on command specific flag, if CLI debug is on.
private static func setupDetailLevelForCommand(commandDebugLogging: Bool, cliDebug: Bool) -> AXLogDetailLevel? {
var previousDetailLevel: AXLogDetailLevel? = nil
if cliDebug { // Only adjust if CLI debugging is already enabled
if commandDebugLogging && GlobalAXLogger.shared.detailLevel != .verbose {
previousDetailLevel = GlobalAXLogger.shared.detailLevel
GlobalAXLogger.shared.detailLevel = .verbose
axDebugLog("[CommandExecutor.setupDetailLevel] Upped detail level to verbose for this command.")
}
} else {
// If CLI debug is not on, command.debugLogging by itself does not turn on logging here.
// AXORCMain is the authority for enabling logging globally via --debug.
// However, if command.debugLogging is true but CLI is not, we might want to enable JUST for this command?
// For now, keeping it simple: CLI --debug is master switch.
}
return previousDetailLevel
}
@MainActor
private static func processCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
switch command.command {
case .performAction:
return handlePerformActionCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .getFocusedElement:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeGetFocusedElement)
case .getAttributes:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeGetAttributes)
case .query:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeQuery)
case .describeElement:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeDescribeElement)
case .extractText:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeExtractText)
case .collectAll:
axDebugLog("CollectAll called. debugCLI=\(debugCLI). Passing to axorcist.handleCollectAll.")
guard let axCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert CollectAll to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for CollectAll")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axCommand))
let handlerResponse: HandlerResponse
if axResponse.status == "success" {
handlerResponse = HandlerResponse(data: axResponse.payload, error: nil)
} else {
handlerResponse = HandlerResponse(data: nil, error: axResponse.error?.message ?? "CollectAll failed")
}
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: handlerResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
case .getElementAtPoint:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI) { cmd, ax in
guard let axCmd = cmd.command.toAXCommand(commandEnvelope: cmd) else {
axErrorLog("Failed to convert GetElementAtPoint to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetElementAtPoint")
}
let axResponse = ax.runCommand(AXCommandEnvelope(commandID: cmd.commandId, command: axCmd))
return HandlerResponse(from: axResponse)
}
case .setFocusedValue:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI) { cmd, ax in
guard let axCmd = cmd.command.toAXCommand(commandEnvelope: cmd) else {
axErrorLog("Failed to convert SetFocusedValue to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for SetFocusedValue")
}
let axResponse = ax.runCommand(AXCommandEnvelope(commandID: cmd.commandId, command: axCmd))
return HandlerResponse(from: axResponse)
}
case .ping:
return handlePingCommand(command: command, debugCLI: debugCLI)
case .batch:
return handleBatchCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .observe:
return handleObserveCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .stopObservation:
// Stop all observations through AXObserverCenter
AXObserverCenter.shared.removeAllObservers()
let stopResponse = FinalResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
status: "success",
data: AnyCodable("All observations stopped"),
error: nil,
debugLogs: debugCLI || command.debugLogging ? axGetLogsAsStrings() : nil
)
return encodeToJson(stopResponse) ?? "{\"error\": \"Encoding stopObservation response failed\", \"commandId\": \"\(command.commandId)\"}"
case .isProcessTrusted:
let trustedResponse = ProcessTrustedResponse(
commandId: command.commandId,
status: "success",
trusted: AXIsProcessTrusted()
)
return encodeToJson(trustedResponse) ?? "{\"error\": \"Encoding isProcessTrusted response failed\", \"commandId\": \"\(command.commandId)\"}"
case .isAXFeatureEnabled:
let axEnabled = AXIsProcessTrustedWithOptions(nil)
let featureEnabledResponse = AXFeatureEnabledResponse(
commandId: command.commandId,
status: "success",
enabled: axEnabled
)
return encodeToJson(featureEnabledResponse) ?? "{\"error\": \"Encoding isAXFeatureEnabled response failed\", \"commandId\": \"\(command.commandId)\"}"
case .setNotificationHandler:
return handleNotImplementedCommand(command: command, message: "setNotificationHandler is not implemented in axorc", debugCLI: debugCLI)
case .removeNotificationHandler:
return handleNotImplementedCommand(command: command, message: "removeNotificationHandler is not implemented in axorc", debugCLI: debugCLI)
case .getElementDescription:
return handleNotImplementedCommand(command: command, message: "getElementDescription is not implemented in axorc", debugCLI: debugCLI)
}
}
}

View File

@ -0,0 +1,118 @@
// CommandHandlers.swift - Command-specific handler functions
import AppKit
import AXorcist
import Foundation
// MARK: - Command Handlers
@MainActor
internal func handlePerformActionCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
guard command.actionName != nil else {
let errorResponse = HandlerResponse(data: nil, error: "performAction requires actionName")
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: errorResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executePerformAction)
}
@MainActor
internal func handleBatchCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
guard let batchCmd = command.command.toAXCommand(commandEnvelope: command) else {
let errorResponse = BatchQueryResponse(commandId: command.commandId, status: "error", message: "Failed to create AXCommand for Batch")
return encodeToJson(errorResponse) ?? "{\"error\": \"Encoding batch response failed\", \"commandId\": \"\(command.commandId)\"}"
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: batchCmd))
var finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "pending")
var logsForResponse: [String]? = nil
if axResponse.status == "success" {
if let batchPayload = axResponse.payload?.value as? BatchResponsePayload {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "success", data: batchPayload.results, errors: batchPayload.errors, debugLogs: nil)
} else {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: "Batch success but payload was not BatchResponsePayload", debugLogs: nil)
}
} else {
let errorMessage = axResponse.error?.message ?? "Batch operation failed with unknown error."
if let batchPayload = axResponse.payload?.value as? BatchResponsePayload {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: errorMessage, data: batchPayload.results, errors: batchPayload.errors, debugLogs: nil)
} else {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: errorMessage, debugLogs: nil)
}
}
if debugCLI || command.debugLogging {
logsForResponse = axGetLogsAsStrings()
finalResponseObject.debugLogs = logsForResponse
}
return encodeToJson(finalResponseObject) ?? "{\"error\": \"Encoding batch response failed\", \"commandId\": \"\(command.commandId)\"}"
}
internal func handlePingCommand(command: CommandEnvelope, debugCLI: Bool) -> String {
axDebugLog("Ping command received. Responding with pong.")
let pingHandlerResponse = HandlerResponse(data: AnyCodable("pong"), error: nil)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: pingHandlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
internal func handleNotImplementedCommand(command: CommandEnvelope, message: String, debugCLI: Bool) -> String {
let notImplementedResponse = HandlerResponse(data: nil, error: message)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: notImplementedResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor
internal func handleObserveCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
guard let axObserveCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Observe to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Observe")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axObserveCommand))
let handlerResponse = HandlerResponse(from: axResponse)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor
internal func handleSimpleCommand(
command: CommandEnvelope,
axorcist: AXorcist,
debugCLI: Bool,
executor: (CommandEnvelope, AXorcist) -> HandlerResponse
) -> String {
let handlerResponse = executor(command, axorcist)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}

View File

@ -0,0 +1,95 @@
// CommandResponseHelpers.swift - Response handling and encoding utilities
import AXorcist
import Foundation
// MARK: - Response Types
struct FinalResponse: Codable {
let commandId: String
let commandType: String
let status: String
let data: AnyCodable?
let error: String?
var debugLogs: [String]?
}
struct ProcessTrustedResponse: Codable {
let commandId: String
let status: String
let trusted: Bool
}
struct AXFeatureEnabledResponse: Codable {
let commandId: String
let status: String
let enabled: Bool
}
// MARK: - Response Helpers
internal func finalizeAndEncodeResponse(
commandId: String,
commandType: String,
handlerResponse: HandlerResponse,
debugCLI: Bool,
commandDebugLogging: Bool
) -> String {
let responseStatus = handlerResponse.error == nil ? "success" : "error"
var finalResponseObject = FinalResponse(
commandId: commandId,
commandType: commandType,
status: responseStatus,
data: handlerResponse.data,
error: handlerResponse.error
)
if debugCLI || commandDebugLogging {
let logsForResponse = axGetLogsAsStrings()
finalResponseObject.debugLogs = logsForResponse
}
return encodeToJson(finalResponseObject) ?? "{\"error\": \"JSON encoding failed\", \"commandId\": \"\(commandId)\"}"
}
internal func encodeToJson<T: Encodable>(_ object: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
do {
let data = try encoder.encode(object)
return String(data: data, encoding: .utf8)
} catch let encodingError as EncodingError {
axErrorLog("JSON encoding failed with EncodingError: \(encodingError.detailedDescription)")
return nil
} catch {
axErrorLog("JSON encoding failed: \(error.localizedDescription)")
return nil
}
}
// Extension for EncodingError details
protocol CodingPathProvider {
var codingPath: [CodingKey] { get }
}
extension EncodingError.Context: CodingPathProvider {}
extension EncodingError {
var detailedDescription: String {
switch self {
case .invalidValue(let value, let context):
return "InvalidValue: '\(value)' attempting to encode at path '\(context.codingPathString)'. Debug: \(context.debugDescription)"
@unknown default:
return "Unknown encoding error. Localized: \(self.localizedDescription)"
}
}
}
// Helper for CodingPathProvider to get a string representation
extension CodingPathProvider {
var codingPathString: String {
codingPath.map { $0.stringValue }.joined(separator: ".")
}
}

View File

@ -0,0 +1,160 @@
// CommandTypeExtensions.swift - Extensions for CommandType conversions
import AXorcist
import Foundation
// MARK: - CommandType Extension for conversion to AXCommand
extension CommandType {
func toAXCommand(commandEnvelope: CommandEnvelope) -> AXCommand? {
switch self {
case .query:
let effectiveLocator = commandEnvelope.locator ?? Locator(criteria: [])
return .query(QueryCommand(
appIdentifier: commandEnvelope.application,
locator: Locator(
matchAll: effectiveLocator.matchAll,
criteria: effectiveLocator.criteria,
rootElementPathHint: effectiveLocator.rootElementPathHint,
descendantCriteria: effectiveLocator.descendantCriteria,
requireAction: effectiveLocator.requireAction,
computedNameContains: effectiveLocator.computedNameContains,
debugPathSearch: commandEnvelope.locator?.debugPathSearch
),
attributesToReturn: commandEnvelope.attributes,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .performAction:
guard let actionName = commandEnvelope.actionName else { return nil }
return .performAction(PerformActionCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
action: actionName,
value: commandEnvelope.actionValue,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .getAttributes:
return .getAttributes(GetAttributesCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
attributes: commandEnvelope.attributes ?? [],
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .describeElement:
return .describeElement(DescribeElementCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
depth: commandEnvelope.maxDepth ?? 3,
includeIgnored: commandEnvelope.includeIgnoredElements ?? false,
maxSearchDepth: commandEnvelope.maxDepth ?? 10
))
case .extractText:
return .extractText(ExtractTextCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
maxDepthForSearch: commandEnvelope.maxDepth ?? 10,
includeChildren: commandEnvelope.includeChildrenInText ?? false,
maxDepth: commandEnvelope.maxDepth
))
case .collectAll:
return .collectAll(CollectAllCommand(
appIdentifier: commandEnvelope.application,
attributesToReturn: commandEnvelope.attributes,
maxDepth: commandEnvelope.maxDepth ?? 10,
filterCriteria: commandEnvelope.filterCriteria,
valueFormatOption: ValueFormatOption.smart
))
case .batch:
guard let batchSubCommands = commandEnvelope.subCommands else {
axErrorLog("toAXCommand: Batch command missing subCommands in CommandEnvelope.")
return nil
}
let axSubCommands = batchSubCommands.compactMap { subCmdEnv -> AXBatchCommand.SubCommandEnvelope? in
guard let axSubCmd = subCmdEnv.command.toAXCommand(commandEnvelope: subCmdEnv) else {
axErrorLog("toAXCommand: Failed to convert subCommand '\(subCmdEnv.commandId)' of type '\(subCmdEnv.command.rawValue)' to AXSubCommand.")
return nil
}
return AXBatchCommand.SubCommandEnvelope(commandID: subCmdEnv.commandId, command: axSubCmd)
}
if axSubCommands.count != batchSubCommands.count {
axErrorLog("toAXCommand: Some subCommands in batch failed to convert. Original: \(batchSubCommands.count), Converted: \(axSubCommands.count)")
}
return .batch(AXBatchCommand(commands: axSubCommands))
case .setFocusedValue:
guard let value = commandEnvelope.actionValue?.value as? String else {
axErrorLog("toAXCommand: SetFocusedValue missing string value in actionValue or wrong type.")
return nil
}
return .setFocusedValue(SetFocusedValueCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
value: value,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .getElementAtPoint:
guard let point = commandEnvelope.point else {
axErrorLog("toAXCommand: GetElementAtPoint missing point.")
return nil
}
return .getElementAtPoint(GetElementAtPointCommand(
point: point,
appIdentifier: commandEnvelope.application,
pid: commandEnvelope.pid,
attributesToReturn: commandEnvelope.attributes,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .getFocusedElement:
return .getFocusedElement(GetFocusedElementCommand(
appIdentifier: commandEnvelope.application,
attributesToReturn: commandEnvelope.attributes,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .observe:
guard let notificationsList = commandEnvelope.notifications, !notificationsList.isEmpty else {
axErrorLog("toAXCommand: Observe missing notifications list.")
return nil
}
guard let firstNotificationName = notificationsList.first,
let axNotification = AXNotification(rawValue: firstNotificationName) else {
axErrorLog("toAXCommand: Invalid or unsupported notification name: \(notificationsList.first ?? "nil") for observe command.")
return nil
}
return .observe(ObserveCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator,
notifications: notificationsList,
includeDetails: true,
watchChildren: commandEnvelope.watchChildren ?? false,
notificationName: axNotification,
includeElementDetails: commandEnvelope.includeElementDetails,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .ping:
return nil
case .stopObservation:
return nil
case .isProcessTrusted:
return nil
case .isAXFeatureEnabled:
return nil
case .setNotificationHandler:
return nil
case .removeNotificationHandler:
return nil
case .getElementDescription:
return nil
}
}
}

View File

@ -1,574 +0,0 @@
// CommandExecutor.swift - Executes AXORC commands
import AppKit // For NSRunningApplication
import AXorcist
import Foundation
// import AXorcist instead of AXorcistLib
// TEMPORARY TEST STRUCT - REMOVED
// struct SimpleTestResponse: Codable {
// var message: String
// var logs: [String]?
// }
@MainActor
struct CommandExecutor {
@MainActor
static func execute(
command: CommandEnvelope,
axorcist: AXorcist,
debugCLI: Bool // This is from the --debug CLI flag
) -> String {
// let (initialLoggingEnabled, initialDetailLevel) = setupLogging(for: command)
// The main AXORCCommand.run() now sets the global logging based on --debug.
// CommandExecutor.setupLogging can adjust detail level if command.debugLogging is true.
let previousDetailLevel = setupDetailLevelForCommand(commandDebugLogging: command.debugLogging, cliDebug: debugCLI)
defer {
// Restore only the detail level if it was changed.
if let prevLevel = previousDetailLevel {
GlobalAXLogger.shared.detailLevel = prevLevel
}
}
// GlobalAXLogger.shared.updateOperationDetails(commandID: command.commandId, appName: command.application) // Commented out for now
axDebugLog("Executing command: \(command.command) (ID: \(command.commandId)), cmdDebug: \(command.debugLogging), cliDebug: \(debugCLI)")
let responseString = processCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
// Logs are cleared by AXORCMain after printing, if appropriate.
// GlobalAXLogger.shared.updateOperationDetails(commandID: nil, appName: nil) // Commented out for now
return responseString
}
// Simplified to only adjust detail level based on command specific flag, if CLI debug is on.
private static func setupDetailLevelForCommand(commandDebugLogging: Bool, cliDebug: Bool) -> AXLogDetailLevel? {
var previousDetailLevel: AXLogDetailLevel? = nil
if cliDebug { // Only adjust if CLI debugging is already enabled
if commandDebugLogging && GlobalAXLogger.shared.detailLevel != .verbose {
previousDetailLevel = GlobalAXLogger.shared.detailLevel
GlobalAXLogger.shared.detailLevel = .verbose
axDebugLog("[CommandExecutor.setupDetailLevel] Upped detail level to verbose for this command.")
}
} else {
// If CLI debug is not on, command.debugLogging by itself does not turn on logging here.
// AXORCMain is the authority for enabling logging globally via --debug.
// However, if command.debugLogging is true but CLI is not, we might want to enable JUST for this command?
// For now, keeping it simple: CLI --debug is master switch.
}
return previousDetailLevel
}
@MainActor
private static func processCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
switch command.command {
case .performAction:
return handlePerformActionCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .getFocusedElement:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeGetFocusedElement)
case .getAttributes:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeGetAttributes)
case .query:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeQuery)
case .describeElement:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeDescribeElement)
case .extractText:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeExtractText)
case .collectAll:
axDebugLog("CollectAll called. debugCLI=\(debugCLI). Passing to axorcist.handleCollectAll.")
guard let axCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert CollectAll to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for CollectAll")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axCommand))
let handlerResponse: HandlerResponse
if axResponse.status == "success" {
handlerResponse = HandlerResponse(data: axResponse.payload, error: nil)
} else {
handlerResponse = HandlerResponse(data: nil, error: axResponse.error?.message ?? "CollectAll failed")
}
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: handlerResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
case .batch:
return handleBatchCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .ping:
return handlePingCommand(command: command, debugCLI: debugCLI)
case .getElementAtPoint:
guard let axCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert GetElementAtPoint to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetElementAtPoint")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axCommand))
let handlerResponse = HandlerResponse(from: axResponse)
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: handlerResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
case .observe:
return handleObserveCommand(command: command, axorcist: axorcist, debugCLI: debugCLI)
case .setFocusedValue:
return handleSimpleCommand(command: command, axorcist: axorcist, debugCLI: debugCLI, executor: executeSetFocusedValue)
case .stopObservation, .isProcessTrusted, .isAXFeatureEnabled, .setNotificationHandler, .removeNotificationHandler, .getElementDescription:
return handleNotImplementedCommand(command: command, message: "Command '\(command.command.rawValue)' is not yet implemented", debugCLI: debugCLI)
}
}
@MainActor
private static func handlePerformActionCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
guard command.actionName != nil else {
let error = "Missing action details for performAction"
axErrorLog(error)
let errorResponse = HandlerResponse(data: nil, error: error)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: errorResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
guard let axCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert PerformAction to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for PerformAction")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axCommand))
let handlerResponse = HandlerResponse(from: axResponse)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor
private static func handleSimpleCommand(
command: CommandEnvelope,
axorcist: AXorcist,
debugCLI: Bool,
executor: (CommandEnvelope, AXorcist) -> HandlerResponse
) -> String {
let handlerResponse = executor(command, axorcist)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor
private static func handleBatchCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
axDebugLog("handleBatchCommand called with debugCLI: \(debugCLI).")
guard command.command == .batch else {
let errorMsg = "Batch command structure is incorrect or not a batch command type."
axErrorLog(errorMsg)
let errorResponse = HandlerResponse(data: nil, error: errorMsg)
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: CommandType.batch.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
guard let axBatchCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Batch to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Batch")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: CommandType.batch.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axBatchCommand))
var finalResponseObject: BatchQueryResponse
var logsForResponse: [String]? = nil
if axResponse.status == "success" {
if let batchPayload = axResponse.payload?.value as? BatchResponsePayload {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "success", data: batchPayload.results, errors: batchPayload.errors, debugLogs: nil)
} else {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: "Batch success but payload was not BatchResponsePayload", debugLogs: nil)
}
} else {
let errorMessage = axResponse.error?.message ?? "Batch operation failed with unknown error."
if let batchPayload = axResponse.payload?.value as? BatchResponsePayload {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: errorMessage, data: batchPayload.results, errors: batchPayload.errors, debugLogs: nil)
} else {
finalResponseObject = BatchQueryResponse(commandId: command.commandId, status: "error", message: errorMessage, debugLogs: nil)
}
}
if debugCLI || command.debugLogging {
logsForResponse = axGetLogsAsStrings()
finalResponseObject.debugLogs = logsForResponse
}
return encodeToJson(finalResponseObject) ?? "{\"error\": \"Encoding batch response failed\", \"commandId\": \"\(command.commandId)\"}"
}
private static func handlePingCommand(command: CommandEnvelope, debugCLI: Bool) -> String {
axDebugLog("Ping command received. Responding with pong.")
let pingHandlerResponse = HandlerResponse(data: AnyCodable("pong"), error: nil)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: pingHandlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
private static func handleNotImplementedCommand(command: CommandEnvelope, message: String, debugCLI: Bool) -> String {
let notImplementedResponse = HandlerResponse(data: nil, error: message)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: notImplementedResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor
private static func handleObserveCommand(command: CommandEnvelope, axorcist: AXorcist, debugCLI: Bool) -> String {
guard let axObserveCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Observe to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Observe")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axObserveCommand))
let handlerResponse = HandlerResponse(from: axResponse)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
// MARK: - Command Execution Functions (now call AXorcist.runCommand)
@MainActor
private static func executeQuery(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axQueryCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Query to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Query")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axQueryCommand))
return HandlerResponse(from: axResponse)
}
@MainActor
private static func executeGetFocusedElement(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axGetFocusedCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert GetFocusedElement to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetFocusedElement")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axGetFocusedCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
private static func executeGetAttributes(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axGetAttrsCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert GetAttributes to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for GetAttributes")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axGetAttrsCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
private static func executeDescribeElement(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axDescribeCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert DescribeElement to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for DescribeElement")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axDescribeCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
private static func executeExtractText(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axExtractCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert ExtractText to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for ExtractText")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axExtractCmd))
return HandlerResponse(from: axResponse)
}
@MainActor
private static func executeSetFocusedValue(command: CommandEnvelope, axorcist: AXorcist) -> HandlerResponse {
guard let axSetFocusedValueCmd = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert SetFocusedValue to AXCommand")
return HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for SetFocusedValue")
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axSetFocusedValueCmd))
return HandlerResponse(from: axResponse)
}
// MARK: - Response Finalization
private static func finalizeAndEncodeResponse(
commandId: String,
commandType: String,
handlerResponse: HandlerResponse,
debugCLI: Bool,
commandDebugLogging: Bool
) -> String {
let dataForResponse: AnyCodable?
if let axElement = handlerResponse.data?.value as? AXElement {
axDebugLog("finalizeAndEncodeResponse: handlerResponse.data contained AXElement. Converting to AXElementForEncoding.")
dataForResponse = AnyCodable(AXElementForEncoding(from: axElement))
} else if handlerResponse.data != nil {
axDebugLog("finalizeAndEncodeResponse: handlerResponse.data was AnyCodable but not AXElement. Passing through. Type: \(type(of: handlerResponse.data!.value))")
dataForResponse = handlerResponse.data // Pass through other AnyCodable types directly
} else {
axDebugLog("finalizeAndEncodeResponse: handlerResponse.data was nil.")
dataForResponse = nil
}
var queryResponse = GenericQueryResponse(
commandId: commandId,
commandType: commandType,
status: handlerResponse.error == nil ? "success" : "error",
data: dataForResponse, // Use the potentially converted data
message: handlerResponse.error
)
if debugCLI || commandDebugLogging {
queryResponse.debugLogs = axGetLogsAsStrings()
} else {
queryResponse.debugLogs = nil
}
let jsonString = encodeToJson(queryResponse)
let fallbackJson = """
{"commandId": "\(commandId)", "commandType": "\(commandType)", "status": "error", "message": "Encoding response failed"}
"""
return jsonString ?? fallbackJson
}
private static func encodeToJson<T: Codable>(_ value: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
do {
let data = try encoder.encode(value)
return String(data: data, encoding: .utf8)
} catch {
let errorMsg = "JSON Encoding Error for type \(String(describing: T.self)): \(error.localizedDescription)"
fputs("ERROR: \(errorMsg)\n", stderr)
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMsg, details: ["error_details": AnyCodable(error.localizedDescription)]))
if let encodingError = error as? EncodingError {
let detailDesc = encodingError.detailedDescription
fputs("ERROR EncodingError Details: \(detailDesc)\n", stderr)
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "EncodingError Details: \(detailDesc)"))
}
fflush(stderr)
return nil
}
}
}
// CommandEnvelope.CommandType to AXCommand conversion
extension CommandType {
func toAXCommand(commandEnvelope: CommandEnvelope) -> AXCommand? {
switch self {
case .query:
let effectiveLocator = commandEnvelope.locator ?? Locator(criteria: [])
return .query(QueryCommand(
appIdentifier: commandEnvelope.application,
locator: Locator(
matchAll: effectiveLocator.matchAll,
criteria: effectiveLocator.criteria,
rootElementPathHint: effectiveLocator.rootElementPathHint,
descendantCriteria: effectiveLocator.descendantCriteria,
requireAction: effectiveLocator.requireAction,
computedNameContains: effectiveLocator.computedNameContains,
debugPathSearch: commandEnvelope.locator?.debugPathSearch
),
attributesToReturn: commandEnvelope.attributes,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .performAction:
guard let actionName = commandEnvelope.actionName else { return nil }
return .performAction(PerformActionCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
action: actionName,
value: commandEnvelope.actionValue,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .getAttributes:
return .getAttributes(GetAttributesCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
attributes: commandEnvelope.attributes ?? [],
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .describeElement:
return .describeElement(DescribeElementCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
depth: commandEnvelope.maxDepth ?? 3,
includeIgnored: commandEnvelope.includeIgnoredElements ?? false,
maxSearchDepth: commandEnvelope.maxDepth ?? 10
))
case .extractText:
return .extractText(ExtractTextCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
maxDepthForSearch: commandEnvelope.maxDepth ?? 10,
includeChildren: commandEnvelope.includeChildrenInText ?? false,
maxDepth: commandEnvelope.maxDepth
))
case .collectAll:
return .collectAll(CollectAllCommand(
appIdentifier: commandEnvelope.application,
attributesToReturn: commandEnvelope.attributes,
maxDepth: commandEnvelope.maxDepth ?? 10,
filterCriteria: commandEnvelope.filterCriteria,
valueFormatOption: ValueFormatOption.smart
))
case .batch:
guard let batchSubCommands = commandEnvelope.subCommands else {
axErrorLog("toAXCommand: Batch command missing subCommands in CommandEnvelope.")
return nil
}
let axSubCommands = batchSubCommands.compactMap { subCmdEnv -> AXBatchCommand.SubCommandEnvelope? in
guard let axSubCmd = subCmdEnv.command.toAXCommand(commandEnvelope: subCmdEnv) else {
axErrorLog("toAXCommand: Failed to convert subCommand '\(subCmdEnv.commandId)' of type '\(subCmdEnv.command.rawValue)' to AXSubCommand.")
return nil
}
return AXBatchCommand.SubCommandEnvelope(commandID: subCmdEnv.commandId, command: axSubCmd)
}
if axSubCommands.count != batchSubCommands.count {
axErrorLog("toAXCommand: Some subCommands in batch failed to convert. Original: \(batchSubCommands.count), Converted: \(axSubCommands.count)")
}
return .batch(AXBatchCommand(commands: axSubCommands))
case .setFocusedValue:
guard let value = commandEnvelope.actionValue?.value as? String else {
axErrorLog("toAXCommand: SetFocusedValue missing string value in actionValue or wrong type.")
return nil
}
return .setFocusedValue(SetFocusedValueCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator ?? Locator(criteria: []),
value: value,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .getElementAtPoint:
guard let point = commandEnvelope.point else {
axErrorLog("toAXCommand: GetElementAtPoint missing point.")
return nil
}
return .getElementAtPoint(GetElementAtPointCommand(
point: point,
appIdentifier: commandEnvelope.application,
pid: commandEnvelope.pid,
attributesToReturn: commandEnvelope.attributes,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .getFocusedElement:
return .getFocusedElement(GetFocusedElementCommand(
appIdentifier: commandEnvelope.application,
attributesToReturn: commandEnvelope.attributes,
includeChildrenBrief: commandEnvelope.includeChildrenBrief
))
case .observe:
guard let notificationsList = commandEnvelope.notifications, !notificationsList.isEmpty else {
axErrorLog("toAXCommand: Observe missing notifications list.")
return nil
}
guard let firstNotificationName = notificationsList.first,
let axNotification = AXNotification(rawValue: firstNotificationName) else {
axErrorLog("toAXCommand: Invalid or unsupported notification name: \(notificationsList.first ?? "nil") for observe command.")
return nil
}
return .observe(ObserveCommand(
appIdentifier: commandEnvelope.application,
locator: commandEnvelope.locator,
notifications: notificationsList,
includeDetails: true,
watchChildren: commandEnvelope.watchChildren ?? false,
notificationName: axNotification,
includeElementDetails: commandEnvelope.includeElementDetails,
maxDepthForSearch: commandEnvelope.maxDepth ?? 10
))
case .ping:
return nil
case .stopObservation:
return nil
case .isProcessTrusted:
return nil
case .isAXFeatureEnabled:
return nil
case .setNotificationHandler:
return nil
case .removeNotificationHandler:
return nil
case .getElementDescription:
return nil
}
}
}
// Extension for EncodingError details
protocol CodingPathProvider {
var codingPath: [CodingKey] { get }
}
extension EncodingError.Context: CodingPathProvider {}
// For DecodingError as well if needed later
// extension DecodingError.Context: CodingPathProvider {}
extension EncodingError {
var detailedDescription: String {
switch self {
case .invalidValue(let value, let context):
return "InvalidValue: '\(value)' attempting to encode at path '\(context.codingPathString)'. Debug: \(context.debugDescription)"
@unknown default:
return "Unknown encoding error. Localized: \(self.localizedDescription)"
}
}
}
// Helper for CodingPathProvider to get a string representation
extension CodingPathProvider {
var codingPathString: String {
codingPath.map { $0.stringValue }.joined(separator: ".")
}
}