break apart large files
This commit is contained in:
parent
dacf59720b
commit
789bde944c
@ -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(
|
||||
|
||||
256
Sources/AXorcist/Core/AXCommands.swift
Normal file
256
Sources/AXorcist/Core/AXCommands.swift
Normal 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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Sources/AXorcist/Core/AnyCodable.swift
Normal file
88
Sources/AXorcist/Core/AnyCodable.swift
Normal 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
|
||||
}
|
||||
}
|
||||
115
Sources/AXorcist/Core/CommandEnvelope.swift
Normal file
115
Sources/AXorcist/Core/CommandEnvelope.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
32
Sources/AXorcist/Core/CommandTypes.swift
Normal file
32
Sources/AXorcist/Core/CommandTypes.swift
Normal 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.
|
||||
}
|
||||
135
Sources/AXorcist/Core/MatchingTypes.swift
Normal file
135
Sources/AXorcist/Core/MatchingTypes.swift
Normal 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
|
||||
}
|
||||
}
|
||||
42
Sources/AXorcist/Core/NotificationTypes.swift
Normal file
42
Sources/AXorcist/Core/NotificationTypes.swift
Normal 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"
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
54
Sources/AXorcist/Core/ObserverHelpers.swift
Normal file
54
Sources/AXorcist/Core/ObserverHelpers.swift
Normal 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
|
||||
}
|
||||
}
|
||||
34
Sources/AXorcist/Core/ObserverTypes.swift
Normal file
34
Sources/AXorcist/Core/ObserverTypes.swift
Normal 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
|
||||
}
|
||||
11
Sources/AXorcist/Core/ValueFormatOption.swift
Normal file
11
Sources/AXorcist/Core/ValueFormatOption.swift
Normal 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.
|
||||
}
|
||||
132
Sources/AXorcist/Search/AttributeBuilders.swift
Normal file
132
Sources/AXorcist/Search/AttributeBuilders.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Sources/AXorcist/Search/AttributeExtractors.swift
Normal file
157
Sources/AXorcist/Search/AttributeExtractors.swift
Normal 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
|
||||
}
|
||||
166
Sources/AXorcist/Search/AttributeFormatters.swift
Normal file
166
Sources/AXorcist/Search/AttributeFormatters.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
*/
|
||||
}
|
||||
34
Sources/AXorcist/Search/AttributeTypes.swift
Normal file
34
Sources/AXorcist/Search/AttributeTypes.swift
Normal 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
|
||||
}
|
||||
23
Sources/AXorcist/Search/GeometryHelpers.swift
Normal file
23
Sources/AXorcist/Search/GeometryHelpers.swift
Normal 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
|
||||
]
|
||||
}
|
||||
86
Sources/AXorcist/Search/PathNavigationCore.swift
Normal file
86
Sources/AXorcist/Search/PathNavigationCore.swift
Normal 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
|
||||
}
|
||||
190
Sources/AXorcist/Search/PathNavigationJSON.swift
Normal file
190
Sources/AXorcist/Search/PathNavigationJSON.swift
Normal 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
|
||||
}
|
||||
76
Sources/AXorcist/Search/PathNavigationMatching.swift
Normal file
76
Sources/AXorcist/Search/PathNavigationMatching.swift
Normal 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)"))
|
||||
}
|
||||
132
Sources/AXorcist/Search/PathNavigationUtilities.swift
Normal file
132
Sources/AXorcist/Search/PathNavigationUtilities.swift
Normal 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
|
||||
}
|
||||
@ -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
|
||||
67
Sources/axorc/CommandExecutionFunctions.swift
Normal file
67
Sources/axorc/CommandExecutionFunctions.swift
Normal 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)
|
||||
}
|
||||
158
Sources/axorc/CommandExecutor.swift
Normal file
158
Sources/axorc/CommandExecutor.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Sources/axorc/CommandHandlers.swift
Normal file
118
Sources/axorc/CommandHandlers.swift
Normal 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
|
||||
)
|
||||
}
|
||||
95
Sources/axorc/CommandResponseHelpers.swift
Normal file
95
Sources/axorc/CommandResponseHelpers.swift
Normal 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: ".")
|
||||
}
|
||||
}
|
||||
160
Sources/axorc/CommandTypeExtensions.swift
Normal file
160
Sources/axorc/CommandTypeExtensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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: ".")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user