Lerge refactor
This commit is contained in:
parent
316d8145f5
commit
ffd784a61d
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "swift",
|
||||
"request": "launch",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder:AXorcist}",
|
||||
"name": "Debug axorc",
|
||||
"program": "${workspaceFolder:AXorcist}/.build/debug/axorc",
|
||||
"preLaunchTask": "swift: Build Debug axorc"
|
||||
},
|
||||
{
|
||||
"type": "swift",
|
||||
"request": "launch",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder:AXorcist}",
|
||||
"name": "Release axorc",
|
||||
"program": "${workspaceFolder:AXorcist}/.build/release/axorc",
|
||||
"preLaunchTask": "swift: Build Release axorc"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,41 +1,5 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "defaults",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/Defaults",
|
||||
"state" : {
|
||||
"revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6",
|
||||
"version" : "7.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keyboardshortcuts",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/KeyboardShortcuts",
|
||||
"state" : {
|
||||
"revision" : "045cf174010beb335fa1d2567d18c057b8787165",
|
||||
"version" : "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "launchatlogin",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sindresorhus/LaunchAtLogin",
|
||||
"state" : {
|
||||
"revision" : "9a894d799269cb591037f9f9cb0961510d4dca81",
|
||||
"version" : "5.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||
"state" : {
|
||||
"revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99",
|
||||
"version" : "2.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
@ -45,15 +9,6 @@
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-log.git",
|
||||
"state" : {
|
||||
"revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa",
|
||||
"version" : "1.6.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
@ -71,15 +26,6 @@
|
||||
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
||||
"version" : "0.99.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "121c146fe591b1320238d054ae35c81ffa45f45a",
|
||||
"version" : "0.12.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@ -39,7 +39,7 @@ extension AXorcist {
|
||||
var cfValue: CFTypeRef?
|
||||
let copyAttributeStatus = AXUIElementCopyAttributeValue(
|
||||
appElement.underlyingElement,
|
||||
kAXFocusedUIElementAttribute as CFString,
|
||||
AXAttributeNames.kAXFocusedUIElementAttribute as CFString,
|
||||
&cfValue
|
||||
)
|
||||
|
||||
|
||||
@ -2,8 +2,7 @@ import AppKit
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Global constant for backwards compatibility
|
||||
public let DEFAULT_MAX_DEPTH_SEARCH = 10
|
||||
// Global constant for backwards compatibility - removed, now using AXMiscConstants.defaultMaxDepthSearch
|
||||
|
||||
// Placeholder for the actual accessibility logic.
|
||||
// For now, this module is very thin and AXorcist.swift is the main public API.
|
||||
@ -15,8 +14,8 @@ public class AXorcist {
|
||||
internal var recursiveCallDebugLogs: [String] = [] // Added for recursive logging
|
||||
|
||||
// Default values for collection and search if not provided by the command
|
||||
public static let defaultMaxDepthSearch = 10 // Default for general locator-based searches
|
||||
public static let defaultMaxDepthCollectAll = 7 // Default for collectAll recursive operations
|
||||
public static let defaultMaxDepthCollectAll = 7 // Default recursion depth for collectAll
|
||||
public static let defaultMaxDepthSearch = 15 // Default recursion depth for search operations
|
||||
public static let defaultMaxDepthPathResolution = 15 // Max depth for resolving path hints
|
||||
public static let defaultMaxDepthDescribe = 5 // ADDED: Default for description recursion
|
||||
public static let defaultTimeoutPerElementCollectAll = 0.5 // seconds
|
||||
@ -70,166 +69,36 @@ public class AXorcist {
|
||||
|
||||
// MARK: - Path Navigation
|
||||
|
||||
// Helper to check if the current element matches a specific attribute-value pair
|
||||
@MainActor
|
||||
private static func currentElementMatchesPathComponent(
|
||||
_ element: Element,
|
||||
attributeName: String,
|
||||
expectedValue: String,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String] // For logging
|
||||
) -> Bool {
|
||||
// Helper to log directly to currentDebugLogs for this function
|
||||
func logLocal(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
if isDebugLoggingEnabled { // Check if logging is enabled for this specific call context
|
||||
let logMessage = AXorcist.formatDebugLogMessage(
|
||||
message,
|
||||
applicationName: nil, // Or pass from navigateToElement if needed
|
||||
commandID: nil, // Or pass from navigateToElement if needed
|
||||
file: file,
|
||||
function: function,
|
||||
line: line
|
||||
)
|
||||
currentDebugLogs.append(logMessage)
|
||||
}
|
||||
}
|
||||
// MARK: - Search Operations
|
||||
|
||||
if attributeName.isEmpty { // Should not happen if parsePathComponent is robust
|
||||
logLocal("currentElementMatchesPathComponent: attributeName is empty, cannot match.")
|
||||
return false
|
||||
}
|
||||
if let actualValue = element.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
// logLocal("currentElementMatchesPathComponent: Element \(element.briefDescription(option: .minimal, isDebugLoggingEnabled: false, currentDebugLogs: ¤tDebugLogs)) has '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
|
||||
if actualValue == expectedValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Updated navigateToElement to prioritize children
|
||||
@MainActor
|
||||
internal func navigateToElement(
|
||||
from startElement: Element,
|
||||
pathHint: [String],
|
||||
public func search(
|
||||
element: Element,
|
||||
locator: Locator,
|
||||
requireAction: String?,
|
||||
depth: Int,
|
||||
maxDepth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Element? {
|
||||
let pathHintString = pathHint.joined(separator: ", ")
|
||||
// Log with the actual isDebugLoggingEnabled value
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Entered. isDebugLoggingEnabled: \(isDebugLoggingEnabled). pathHint: [\(pathHintString)]", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
|
||||
func dLog(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
// Use the passed-in isDebugLoggingEnabled
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: file, function: function, line: line))
|
||||
}
|
||||
) -> (foundElement: Element?, logs: [String]) {
|
||||
// Initial log for this AXorcist-level search call
|
||||
if isDebugLoggingEnabled {
|
||||
let initialMessage = "AXorcist.search called with locator: \(locator.criteria), path_hint: \(locator.root_element_path_hint ?? [])"
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(initialMessage, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
|
||||
var currentElement = startElement
|
||||
var currentPathSegmentForLog = ""
|
||||
// Call the global findElementViaPathAndCriteria
|
||||
// Note: findElementViaPathAndCriteria will handle its own detailed logging (dLog to currentDebugLogs if !JSON_LOG, or writeSearchLogEntry to stderr if JSON_LOG)
|
||||
let foundElement = findElementViaPathAndCriteria(
|
||||
application: element,
|
||||
locator: locator,
|
||||
maxDepth: maxDepth, // Assuming 'depth' passed to AXorcist.search is for initial call, maxDepth for traversal
|
||||
isDebugLoggingEnabledParam: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs // Pass this along for findElementViaPathAndCriteria to use
|
||||
)
|
||||
|
||||
for (index, pathComponentString) in pathHint.enumerated() {
|
||||
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
|
||||
let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
dLog("Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)")
|
||||
|
||||
let (attributeName, expectedValue) = PathUtils.parsePathComponent(pathComponentString)
|
||||
guard !attributeName.isEmpty else {
|
||||
dLog("CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty attribute name from pathComponentString '\(pathComponentString)'")
|
||||
return nil
|
||||
}
|
||||
|
||||
var foundMatchForThisComponent = false
|
||||
var newElementForNextStep: Element? = nil
|
||||
|
||||
// Priority 1: Check children using Element.children()
|
||||
if let childrenFromElementDotChildren = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)")
|
||||
for child in childrenFromElementDotChildren {
|
||||
let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
if let actualValue = child.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog(" [Nav Child Check 1] Child: \(childBriefDescForLog), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
|
||||
if actualValue == expectedValue {
|
||||
dLog("Matched child (from Element.children): \(childBriefDescForLog) for '\(attributeName):\(expectedValue)'")
|
||||
newElementForNextStep = child
|
||||
foundMatchForThisComponent = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// dLog("Attribute '\(attributeName)' was nil for child (from Element.children): \(childBriefDescForLog)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dLog("Current element \(briefDesc) has no children from Element.children() or children array was nil.")
|
||||
}
|
||||
|
||||
// FALLBACK: If no child matched via Element.children(), try direct kAXChildrenAttribute call (Heisenbug workaround)
|
||||
if !foundMatchForThisComponent {
|
||||
// Log entry for this fallback block, without using currentElement.briefDescription() before the critical call.
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: No match from Element.children(). Trying direct kAXChildrenAttribute fallback.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
|
||||
var directChildrenValue: CFTypeRef?
|
||||
let directChildrenError = AXUIElementCopyAttributeValue(currentElement.underlyingElement, kAXChildrenAttribute as CFString, &directChildrenValue)
|
||||
|
||||
// Now, after the critical call, we can get the description for logging.
|
||||
let currentElementDescForFallbackLog = isDebugLoggingEnabled ? currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) : "Element(debug_off)"
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Fallback is for element: \(currentElementDescForFallbackLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
|
||||
if directChildrenError == .success, let cfArray = directChildrenValue, CFGetTypeID(cfArray) == CFArrayGetTypeID() {
|
||||
if let directAxElements = cfArray as? [AXUIElement] {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback found \(directAxElements.count) raw children for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
for axChild in directAxElements {
|
||||
let childElement = Element(axChild)
|
||||
let childBriefDescForLogFallback = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
if let actualValue = childElement.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog(" [Nav Child Check 2-Fallback] Child: \(childBriefDescForLogFallback), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
|
||||
if actualValue == expectedValue {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Matched child (from direct fallback) for '\(attributeName):\(expectedValue)' on \(currentElementDescForFallbackLog). Child: \(childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
newElementForNextStep = childElement
|
||||
foundMatchForThisComponent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: CFArray failed to cast to [AXUIElement] for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
} else if directChildrenError != .success {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: Error fetching for \(currentElementDescForFallbackLog): \(directChildrenError.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct kAXChildrenAttribute fallback: No children or not an array for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: If no child matched (even after fallback), check current element itself
|
||||
if !foundMatchForThisComponent {
|
||||
var tempLogsForMatchCheck = currentDebugLogs
|
||||
let matchResult = AXorcist.currentElementMatchesPathComponent(
|
||||
currentElement,
|
||||
attributeName: attributeName,
|
||||
expectedValue: expectedValue,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForMatchCheck
|
||||
)
|
||||
currentDebugLogs = tempLogsForMatchCheck
|
||||
|
||||
if matchResult {
|
||||
dLog("Current element \(briefDesc) itself matches '\(attributeName):\(expectedValue)'. Retaining current element for this step.")
|
||||
newElementForNextStep = currentElement
|
||||
foundMatchForThisComponent = true
|
||||
}
|
||||
}
|
||||
|
||||
if foundMatchForThisComponent, let nextElement = newElementForNextStep {
|
||||
currentElement = nextElement
|
||||
} else {
|
||||
dLog("Neither current element \(briefDesc) nor its children (after all checks) matched '\(attributeName):\(expectedValue)'. Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
return currentElement
|
||||
// The currentDebugLogs array has been populated by findElementViaPathAndCriteria (if JSON logging is off)
|
||||
// or contains only the initial logs from this function if JSON logging is on.
|
||||
return (foundElement: foundElement, logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
17
Sources/AXorcist/Core/AXActionNameConstants.swift
Normal file
17
Sources/AXorcist/Core/AXActionNameConstants.swift
Normal file
@ -0,0 +1,17 @@
|
||||
// AXActionNameConstants.swift - Accessibility action name constants
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AXActionNames {
|
||||
public static let kAXIncrementAction = "AXIncrement" // New
|
||||
public static let kAXDecrementAction = "AXDecrement" // New
|
||||
public static let kAXConfirmAction = "AXConfirm" // New
|
||||
public static let kAXCancelAction = "AXCancel" // New
|
||||
public static let kAXShowMenuAction = "AXShowMenu"
|
||||
public static let kAXPickAction = "AXPick" // New (Obsolete in headers, but sometimes seen)
|
||||
public static let kAXPressAction = "AXPress" // New
|
||||
public static let kAXRaiseAction = "AXRaise" // New
|
||||
|
||||
// Specific action name for setting a value, used internally by performActionOnElement
|
||||
public static let kAXSetValueAction = "AXSetValue"
|
||||
}
|
||||
112
Sources/AXorcist/Core/AXAttributeNameConstants.swift
Normal file
112
Sources/AXorcist/Core/AXAttributeNameConstants.swift
Normal file
@ -0,0 +1,112 @@
|
||||
// AXAttributeNameConstants.swift - Accessibility attribute name constants
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AXAttributeNames {
|
||||
// Standard Accessibility Attributes - Values should match CFSTR defined in AXAttributeConstants.h
|
||||
public static let kAXRoleAttribute = "AXRole" // Reverted to String literal
|
||||
public static let kAXSubroleAttribute = "AXSubrole"
|
||||
public static let kAXRoleDescriptionAttribute = "AXRoleDescription"
|
||||
public static let kAXTitleAttribute = "AXTitle"
|
||||
public static let kAXValueAttribute = "AXValue"
|
||||
public static let kAXValueDescriptionAttribute = "AXValueDescription" // New
|
||||
public static let kAXDescriptionAttribute = "AXDescription"
|
||||
public static let kAXHelpAttribute = "AXHelp"
|
||||
public static let kAXIdentifierAttribute = "AXIdentifier"
|
||||
public static let kAXPlaceholderValueAttribute = "AXPlaceholderValue"
|
||||
public static let kAXLabelUIElementAttribute = "AXLabelUIElement"
|
||||
public static let kAXTitleUIElementAttribute = "AXTitleUIElement"
|
||||
public static let kAXLabelValueAttribute = "AXLabelValue"
|
||||
public static let kAXElementBusyAttribute = "AXElementBusy" // New
|
||||
public static let kAXAlternateUIVisibleAttribute = "AXAlternateUIVisible" // New
|
||||
|
||||
public static let kAXChildrenAttribute = "AXChildren"
|
||||
public static let kAXParentAttribute = "AXParent"
|
||||
public static let kAXWindowsAttribute = "AXWindows"
|
||||
public static let kAXMainWindowAttribute = "AXMainWindow"
|
||||
public static let kAXFocusedWindowAttribute = "AXFocusedWindow"
|
||||
public static let kAXFocusedUIElementAttribute = "AXFocusedUIElement"
|
||||
|
||||
public static let kAXEnabledAttribute = "AXEnabled"
|
||||
public static let kAXFocusedAttribute = "AXFocused"
|
||||
public static let kAXMainAttribute = "AXMain" // Window-specific
|
||||
public static let kAXMinimizedAttribute = "AXMinimized" // New, Window-specific
|
||||
public static let kAXCloseButtonAttribute = "AXCloseButton" // New, Window-specific
|
||||
public static let kAXZoomButtonAttribute = "AXZoomButton" // New, Window-specific
|
||||
public static let kAXMinimizeButtonAttribute = "AXMinimizeButton" // New, Window-specific
|
||||
public static let kAXFullScreenButtonAttribute = "AXFullScreenButton" // New, Window-specific
|
||||
public static let kAXDefaultButtonAttribute = "AXDefaultButton" // New, Window-specific
|
||||
public static let kAXCancelButtonAttribute = "AXCancelButton" // New, Window-specific
|
||||
public static let kAXGrowAreaAttribute = "AXGrowArea" // New, Window-specific
|
||||
public static let kAXModalAttribute = "AXModal" // New, Window-specific
|
||||
|
||||
public static let kAXMenuBarAttribute = "AXMenuBar" // New, App-specific
|
||||
public static let kAXFrontmostAttribute = "AXFrontmost" // New, App-specific
|
||||
public static let kAXHiddenAttribute = "AXHidden" // New, App-specific
|
||||
|
||||
public static let kAXPositionAttribute = "AXPosition"
|
||||
public static let kAXSizeAttribute = "AXSize"
|
||||
|
||||
// Value attributes
|
||||
public static let kAXMinValueAttribute = "AXMinValue" // New
|
||||
public static let kAXMaxValueAttribute = "AXMaxValue" // New
|
||||
public static let kAXValueIncrementAttribute = "AXValueIncrement" // New
|
||||
public static let kAXAllowedValuesAttribute = "AXAllowedValues" // New
|
||||
|
||||
// Text-specific attributes
|
||||
public static let kAXSelectedTextAttribute = "AXSelectedText" // New
|
||||
public static let kAXSelectedTextRangeAttribute = "AXSelectedTextRange" // New
|
||||
public static let kAXNumberOfCharactersAttribute = "AXNumberOfCharacters" // New
|
||||
public static let kAXVisibleCharacterRangeAttribute = "AXVisibleCharacterRange" // New
|
||||
public static let kAXInsertionPointLineNumberAttribute = "AXInsertionPointLineNumber" // New
|
||||
|
||||
// Actions - Values should match CFSTR defined in AXActionConstants.h
|
||||
public static let kAXActionsAttribute = "AXActions" // This is actually kAXActionNamesAttribute typically
|
||||
public static let kAXActionNamesAttribute = "AXActionNames" // Correct name for listing actions
|
||||
public static let kAXActionDescriptionAttribute =
|
||||
"AXActionDescription" // To get desc of an action (not in AXActionConstants.h but AXUIElement.h)
|
||||
|
||||
// Attributes for web content and tables/lists
|
||||
public static let kAXVisibleChildrenAttribute = "AXVisibleChildren"
|
||||
public static let kAXSelectedChildrenAttribute = "AXSelectedChildren"
|
||||
public static let kAXTabsAttribute = "AXTabs" // Often a kAXRadioGroup or kAXTabGroup role
|
||||
public static let kAXRowsAttribute = "AXRows"
|
||||
public static let kAXColumnsAttribute = "AXColumns"
|
||||
public static let kAXSelectedRowsAttribute = "AXSelectedRows" // New
|
||||
public static let kAXSelectedColumnsAttribute = "AXSelectedColumns" // New
|
||||
public static let kAXIndexAttribute = "AXIndex" // New (for rows/columns)
|
||||
public static let kAXDisclosingAttribute = "AXDisclosing" // New (for outlines)
|
||||
|
||||
// Custom or less standard attributes (verify usage and standard names)
|
||||
public static let kAXPathHintAttribute = "AXPathHint" // Our custom attribute for pathing
|
||||
|
||||
// DOM specific attributes (these seem custom or web-specific, not standard Apple AX)
|
||||
// Verify if these are actual attribute names exposed by web views or custom implementations.
|
||||
public static let kAXDOMIdentifierAttribute = "AXDOMIdentifier" // Example, might not be standard AX
|
||||
public static let kAXDOMClassListAttribute = "AXDOMClassList" // Example, might not be standard AX
|
||||
public static let kAXARIADOMResourceAttribute = "AXARIADOMResource" // Example
|
||||
public static let kAXARIADOMFunctionAttribute = "AXARIADOM-función" // Corrected identifier, kept original string value.
|
||||
public static let kAXARIADOMChildrenAttribute = "AXARIADOMChildren" // New
|
||||
public static let kAXDOMChildrenAttribute = "AXDOMChildren" // New
|
||||
|
||||
// New constants for missing attributes
|
||||
public static let kAXToolbarButtonAttribute = "AXToolbarButton"
|
||||
public static let kAXProxyAttribute = "AXProxy"
|
||||
public static let kAXSelectedCellsAttribute = "AXSelectedCells"
|
||||
public static let kAXHeaderAttribute = "AXHeader"
|
||||
public static let kAXHorizontalScrollBarAttribute = "AXHorizontalScrollBar"
|
||||
public static let kAXVerticalScrollBarAttribute = "AXVerticalScrollBar"
|
||||
|
||||
// Attributes used in child heuristic collection (often non-standard or specific)
|
||||
public static let kAXWebAreaChildrenAttribute = "AXWebAreaChildren"
|
||||
public static let kAXHTMLContentAttribute = "AXHTMLContent"
|
||||
public static let kAXApplicationNavigationAttribute = "AXApplicationNavigation"
|
||||
public static let kAXApplicationElementsAttribute = "AXApplicationElements"
|
||||
public static let kAXContentsAttribute = "AXContents"
|
||||
public static let kAXBodyAreaAttribute = "AXBodyArea"
|
||||
public static let kAXDocumentContentAttribute = "AXDocumentContent"
|
||||
public static let kAXWebPageContentAttribute = "AXWebPageContent"
|
||||
public static let kAXSplitGroupContentsAttribute = "AXSplitGroupContents"
|
||||
public static let kAXLayoutAreaChildrenAttribute = "AXLayoutAreaChildren"
|
||||
public static let kAXGroupChildrenAttribute = "AXGroupChildren"
|
||||
}
|
||||
23
Sources/AXorcist/Core/AXMiscConstants.swift
Normal file
23
Sources/AXorcist/Core/AXMiscConstants.swift
Normal file
@ -0,0 +1,23 @@
|
||||
// AXMiscConstants.swift - Miscellaneous accessibility constants
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AXMiscConstants {
|
||||
// Configuration Constants
|
||||
public static let maxCollectAllHits = 200 // Default max elements for collect_all if not specified in command
|
||||
public static let defaultMaxDepthSearch = 20 // Default max recursion depth for search
|
||||
public static let defaultMaxDepthCollectAll = 15 // Default max recursion depth for collect_all
|
||||
public static let axBinaryVersion = "1.1.7" // Updated version
|
||||
public static let binaryVersion = "1.1.7" // Updated version without AX prefix
|
||||
|
||||
// String constant for "not available"
|
||||
public static let kAXNotAvailableString = "n/a"
|
||||
|
||||
// MARK: - Custom Application/Computed Keys
|
||||
|
||||
public static let focusedApplicationKey = "focused"
|
||||
public static let computedNameAttributeKey = "ComputedName"
|
||||
public static let isClickableAttributeKey = "IsClickable"
|
||||
public static let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
|
||||
public static let computedPathAttributeKey = "ComputedPath"
|
||||
}
|
||||
43
Sources/AXorcist/Core/AXRoleNameConstants.swift
Normal file
43
Sources/AXorcist/Core/AXRoleNameConstants.swift
Normal file
@ -0,0 +1,43 @@
|
||||
// AXRoleNameConstants.swift - Accessibility role name constants
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AXRoleNames {
|
||||
// Standard Accessibility Roles - Values should match CFSTR defined in AXRoleConstants.h (examples, add more as needed)
|
||||
public static let kAXApplicationRole = "AXApplication"
|
||||
public static let kAXSystemWideRole = "AXSystemWide" // New
|
||||
public static let kAXWindowRole = "AXWindow"
|
||||
public static let kAXSheetRole = "AXSheet" // New
|
||||
public static let kAXDrawerRole = "AXDrawer" // New
|
||||
public static let kAXGroupRole = "AXGroup"
|
||||
public static let kAXButtonRole = "AXButton"
|
||||
public static let kAXRadioButtonRole = "AXRadioButton" // New
|
||||
public static let kAXCheckBoxRole = "AXCheckBox"
|
||||
public static let kAXPopUpButtonRole = "AXPopUpButton" // New
|
||||
public static let kAXMenuButtonRole = "AXMenuButton" // New
|
||||
public static let kAXStaticTextRole = "AXStaticText"
|
||||
public static let kAXTextFieldRole = "AXTextField"
|
||||
public static let kAXTextAreaRole = "AXTextArea"
|
||||
public static let kAXScrollAreaRole = "AXScrollArea"
|
||||
public static let kAXScrollBarRole = "AXScrollBar" // New
|
||||
public static let kAXWebAreaRole = "AXWebArea"
|
||||
public static let kAXImageRole = "AXImage" // New
|
||||
public static let kAXListRole = "AXList" // New
|
||||
public static let kAXTableRole = "AXTable" // New
|
||||
public static let kAXOutlineRole = "AXOutline" // New
|
||||
public static let kAXColumnRole = "AXColumn" // New
|
||||
public static let kAXRowRole = "AXRow" // New
|
||||
public static let kAXToolbarRole = "AXToolbar"
|
||||
public static let kAXBusyIndicatorRole = "AXBusyIndicator" // New
|
||||
public static let kAXProgressIndicatorRole = "AXProgressIndicator" // New
|
||||
public static let kAXSliderRole = "AXSlider" // New
|
||||
public static let kAXIncrementorRole = "AXIncrementor" // New
|
||||
public static let kAXDisclosureTriangleRole = "AXDisclosureTriangle" // New
|
||||
public static let kAXMenuRole = "AXMenu" // New
|
||||
public static let kAXMenuItemRole = "AXMenuItem" // New
|
||||
public static let kAXSplitGroupRole = "AXSplitGroup" // New
|
||||
public static let kAXSplitterRole = "AXSplitter" // New
|
||||
public static let kAXColorWellRole = "AXColorWell" // New
|
||||
public static let kAXLinkRole = "AXLink" // Added for consistency
|
||||
public static let kAXUnknownRole = "AXUnknown" // New
|
||||
}
|
||||
@ -1,203 +0,0 @@
|
||||
// AccessibilityConstants.swift - Defines global constants used throughout the accessibility helper
|
||||
|
||||
import AppKit // Added for NSAccessibility
|
||||
import ApplicationServices // Added for AXError type
|
||||
import Foundation
|
||||
|
||||
// Configuration Constants
|
||||
public let maxCollectAllHits = 200 // Default max elements for collect_all if not specified in command
|
||||
public let defaultMaxDepthSearch = 20 // Default max recursion depth for search
|
||||
public let defaultMaxDepthCollectAll = 15 // Default max recursion depth for collect_all
|
||||
public let axBinaryVersion = "1.1.7" // Updated version
|
||||
public let binaryVersion = "1.1.7" // Updated version without AX prefix
|
||||
|
||||
// Standard Accessibility Attributes - Values should match CFSTR defined in AXAttributeConstants.h
|
||||
public let kAXRoleAttribute = "AXRole" // Reverted to String literal
|
||||
public let kAXSubroleAttribute = "AXSubrole"
|
||||
public let kAXRoleDescriptionAttribute = "AXRoleDescription"
|
||||
public let kAXTitleAttribute = "AXTitle"
|
||||
public let kAXValueAttribute = "AXValue"
|
||||
public let kAXValueDescriptionAttribute = "AXValueDescription" // New
|
||||
public let kAXDescriptionAttribute = "AXDescription"
|
||||
public let kAXHelpAttribute = "AXHelp"
|
||||
public let kAXIdentifierAttribute = "AXIdentifier"
|
||||
public let kAXPlaceholderValueAttribute = "AXPlaceholderValue"
|
||||
public let kAXLabelUIElementAttribute = "AXLabelUIElement"
|
||||
public let kAXTitleUIElementAttribute = "AXTitleUIElement"
|
||||
public let kAXLabelValueAttribute = "AXLabelValue"
|
||||
public let kAXElementBusyAttribute = "AXElementBusy" // New
|
||||
public let kAXAlternateUIVisibleAttribute = "AXAlternateUIVisible" // New
|
||||
|
||||
public let kAXChildrenAttribute = "AXChildren"
|
||||
public let kAXParentAttribute = "AXParent"
|
||||
public let kAXWindowsAttribute = "AXWindows"
|
||||
public let kAXMainWindowAttribute = "AXMainWindow"
|
||||
public let kAXFocusedWindowAttribute = "AXFocusedWindow"
|
||||
public let kAXFocusedUIElementAttribute = "AXFocusedUIElement"
|
||||
|
||||
public let kAXEnabledAttribute = "AXEnabled"
|
||||
public let kAXFocusedAttribute = "AXFocused"
|
||||
public let kAXMainAttribute = "AXMain" // Window-specific
|
||||
public let kAXMinimizedAttribute = "AXMinimized" // New, Window-specific
|
||||
public let kAXCloseButtonAttribute = "AXCloseButton" // New, Window-specific
|
||||
public let kAXZoomButtonAttribute = "AXZoomButton" // New, Window-specific
|
||||
public let kAXMinimizeButtonAttribute = "AXMinimizeButton" // New, Window-specific
|
||||
public let kAXFullScreenButtonAttribute = "AXFullScreenButton" // New, Window-specific
|
||||
public let kAXDefaultButtonAttribute = "AXDefaultButton" // New, Window-specific
|
||||
public let kAXCancelButtonAttribute = "AXCancelButton" // New, Window-specific
|
||||
public let kAXGrowAreaAttribute = "AXGrowArea" // New, Window-specific
|
||||
public let kAXModalAttribute = "AXModal" // New, Window-specific
|
||||
|
||||
public let kAXMenuBarAttribute = "AXMenuBar" // New, App-specific
|
||||
public let kAXFrontmostAttribute = "AXFrontmost" // New, App-specific
|
||||
public let kAXHiddenAttribute = "AXHidden" // New, App-specific
|
||||
|
||||
public let kAXPositionAttribute = "AXPosition"
|
||||
public let kAXSizeAttribute = "AXSize"
|
||||
|
||||
// Value attributes
|
||||
public let kAXMinValueAttribute = "AXMinValue" // New
|
||||
public let kAXMaxValueAttribute = "AXMaxValue" // New
|
||||
public let kAXValueIncrementAttribute = "AXValueIncrement" // New
|
||||
public let kAXAllowedValuesAttribute = "AXAllowedValues" // New
|
||||
|
||||
// Text-specific attributes
|
||||
public let kAXSelectedTextAttribute = "AXSelectedText" // New
|
||||
public let kAXSelectedTextRangeAttribute = "AXSelectedTextRange" // New
|
||||
public let kAXNumberOfCharactersAttribute = "AXNumberOfCharacters" // New
|
||||
public let kAXVisibleCharacterRangeAttribute = "AXVisibleCharacterRange" // New
|
||||
public let kAXInsertionPointLineNumberAttribute = "AXInsertionPointLineNumber" // New
|
||||
|
||||
// Actions - Values should match CFSTR defined in AXActionConstants.h
|
||||
public let kAXActionsAttribute = "AXActions" // This is actually kAXActionNamesAttribute typically
|
||||
public let kAXActionNamesAttribute = "AXActionNames" // Correct name for listing actions
|
||||
public let kAXActionDescriptionAttribute =
|
||||
"AXActionDescription" // To get desc of an action (not in AXActionConstants.h but AXUIElement.h)
|
||||
|
||||
public let kAXIncrementAction = "AXIncrement" // New
|
||||
public let kAXDecrementAction = "AXDecrement" // New
|
||||
public let kAXConfirmAction = "AXConfirm" // New
|
||||
public let kAXCancelAction = "AXCancel" // New
|
||||
public let kAXShowMenuAction = "AXShowMenu"
|
||||
public let kAXPickAction = "AXPick" // New (Obsolete in headers, but sometimes seen)
|
||||
public let kAXPressAction = "AXPress" // New
|
||||
|
||||
// Specific action name for setting a value, used internally by performActionOnElement
|
||||
public let kAXSetValueAction = "AXSetValue"
|
||||
|
||||
// Standard Accessibility Roles - Values should match CFSTR defined in AXRoleConstants.h (examples, add more as needed)
|
||||
public let kAXApplicationRole = "AXApplication"
|
||||
public let kAXSystemWideRole = "AXSystemWide" // New
|
||||
public let kAXWindowRole = "AXWindow"
|
||||
public let kAXSheetRole = "AXSheet" // New
|
||||
public let kAXDrawerRole = "AXDrawer" // New
|
||||
public let kAXGroupRole = "AXGroup"
|
||||
public let kAXButtonRole = "AXButton"
|
||||
public let kAXRadioButtonRole = "AXRadioButton" // New
|
||||
public let kAXCheckBoxRole = "AXCheckBox"
|
||||
public let kAXPopUpButtonRole = "AXPopUpButton" // New
|
||||
public let kAXMenuButtonRole = "AXMenuButton" // New
|
||||
public let kAXStaticTextRole = "AXStaticText"
|
||||
public let kAXTextFieldRole = "AXTextField"
|
||||
public let kAXTextAreaRole = "AXTextArea"
|
||||
public let kAXScrollAreaRole = "AXScrollArea"
|
||||
public let kAXScrollBarRole = "AXScrollBar" // New
|
||||
public let kAXWebAreaRole = "AXWebArea"
|
||||
public let kAXImageRole = "AXImage" // New
|
||||
public let kAXListRole = "AXList" // New
|
||||
public let kAXTableRole = "AXTable" // New
|
||||
public let kAXOutlineRole = "AXOutline" // New
|
||||
public let kAXColumnRole = "AXColumn" // New
|
||||
public let kAXRowRole = "AXRow" // New
|
||||
public let kAXToolbarRole = "AXToolbar"
|
||||
public let kAXBusyIndicatorRole = "AXBusyIndicator" // New
|
||||
public let kAXProgressIndicatorRole = "AXProgressIndicator" // New
|
||||
public let kAXSliderRole = "AXSlider" // New
|
||||
public let kAXIncrementorRole = "AXIncrementor" // New
|
||||
public let kAXDisclosureTriangleRole = "AXDisclosureTriangle" // New
|
||||
public let kAXMenuRole = "AXMenu" // New
|
||||
public let kAXMenuItemRole = "AXMenuItem" // New
|
||||
public let kAXSplitGroupRole = "AXSplitGroup" // New
|
||||
public let kAXSplitterRole = "AXSplitter" // New
|
||||
public let kAXColorWellRole = "AXColorWell" // New
|
||||
public let kAXLinkRole = "AXLink" // New
|
||||
public let kAXUnknownRole = "AXUnknown" // New
|
||||
|
||||
// Attributes for web content and tables/lists
|
||||
public let kAXVisibleChildrenAttribute = "AXVisibleChildren"
|
||||
public let kAXSelectedChildrenAttribute = "AXSelectedChildren"
|
||||
public let kAXTabsAttribute = "AXTabs" // Often a kAXRadioGroup or kAXTabGroup role
|
||||
public let kAXRowsAttribute = "AXRows"
|
||||
public let kAXColumnsAttribute = "AXColumns"
|
||||
public let kAXSelectedRowsAttribute = "AXSelectedRows" // New
|
||||
public let kAXSelectedColumnsAttribute = "AXSelectedColumns" // New
|
||||
public let kAXIndexAttribute = "AXIndex" // New (for rows/columns)
|
||||
public let kAXDisclosingAttribute = "AXDisclosing" // New (for outlines)
|
||||
|
||||
// Custom or less standard attributes (verify usage and standard names)
|
||||
public let kAXPathHintAttribute = "AXPathHint" // Our custom attribute for pathing
|
||||
|
||||
// String constant for "not available"
|
||||
public let kAXNotAvailableString = "n/a"
|
||||
|
||||
// DOM specific attributes (these seem custom or web-specific, not standard Apple AX)
|
||||
// Verify if these are actual attribute names exposed by web views or custom implementations.
|
||||
public let kAXDOMIdentifierAttribute = "AXDOMIdentifier" // Example, might not be standard AX
|
||||
public let kAXDOMClassListAttribute = "AXDOMClassList" // Example, might not be standard AX
|
||||
public let kAXARIADOMResourceAttribute = "AXARIADOMResource" // Example
|
||||
public let kAXARIADOMFunctionAttribute = "AXARIADOM-función" // Corrected identifier, kept original string value.
|
||||
public let kAXARIADOMChildrenAttribute = "AXARIADOMChildren" // New
|
||||
public let kAXDOMChildrenAttribute = "AXDOMChildren" // New
|
||||
|
||||
// New constants for missing attributes
|
||||
public let kAXToolbarButtonAttribute = "AXToolbarButton"
|
||||
public let kAXProxyAttribute = "AXProxy"
|
||||
public let kAXSelectedCellsAttribute = "AXSelectedCells"
|
||||
public let kAXHeaderAttribute = "AXHeader"
|
||||
public let kAXHorizontalScrollBarAttribute = "AXHorizontalScrollBar"
|
||||
public let kAXVerticalScrollBarAttribute = "AXVerticalScrollBar"
|
||||
|
||||
// Attributes used in child heuristic collection (often non-standard or specific)
|
||||
public let kAXWebAreaChildrenAttribute = "AXWebAreaChildren"
|
||||
public let kAXHTMLContentAttribute = "AXHTMLContent"
|
||||
public let kAXApplicationNavigationAttribute = "AXApplicationNavigation"
|
||||
public let kAXApplicationElementsAttribute = "AXApplicationElements"
|
||||
public let kAXContentsAttribute = "AXContents"
|
||||
public let kAXBodyAreaAttribute = "AXBodyArea"
|
||||
public let kAXDocumentContentAttribute = "AXDocumentContent"
|
||||
public let kAXWebPageContentAttribute = "AXWebPageContent"
|
||||
public let kAXSplitGroupContentsAttribute = "AXSplitGroupContents"
|
||||
public let kAXLayoutAreaChildrenAttribute = "AXLayoutAreaChildren"
|
||||
public let kAXGroupChildrenAttribute = "AXGroupChildren"
|
||||
|
||||
// Helper function to convert AXError to a string
|
||||
public func axErrorToString(_ error: AXError) -> String {
|
||||
switch error {
|
||||
case .success: return "success"
|
||||
case .failure: return "failure"
|
||||
case .apiDisabled: return "apiDisabled"
|
||||
case .invalidUIElement: return "invalidUIElement"
|
||||
case .invalidUIElementObserver: return "invalidUIElementObserver"
|
||||
case .cannotComplete: return "cannotComplete"
|
||||
case .attributeUnsupported: return "attributeUnsupported"
|
||||
case .actionUnsupported: return "actionUnsupported"
|
||||
case .notificationUnsupported: return "notificationUnsupported"
|
||||
case .notImplemented: return "notImplemented"
|
||||
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
|
||||
case .notificationNotRegistered: return "notificationNotRegistered"
|
||||
case .noValue: return "noValue"
|
||||
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
|
||||
case .notEnoughPrecision: return "notEnoughPrecision"
|
||||
case .illegalArgument: return "illegalArgument"
|
||||
@unknown default:
|
||||
return "unknown AXError (code: \(error.rawValue))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Application/Computed Keys
|
||||
|
||||
public let focusedApplicationKey = "focused"
|
||||
public let computedNameAttributeKey = "ComputedName"
|
||||
public let isClickableAttributeKey = "IsClickable"
|
||||
public let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
|
||||
public let computedPathAttributeKey = "ComputedPath"
|
||||
89
Sources/AXorcist/Core/AnyCodable.swift
Normal file
89
Sources/AXorcist/Core/AnyCodable.swift
Normal file
@ -0,0 +1,89 @@
|
||||
import Foundation
|
||||
|
||||
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
|
||||
public struct AnyCodable: Codable {
|
||||
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 int32 = try? container.decode(Int32.self) {
|
||||
self.value = int32
|
||||
} else if let int64 = try? container.decode(Int64.self) {
|
||||
self.value = int64
|
||||
} else if let uint = try? container.decode(UInt.self) {
|
||||
self.value = uint
|
||||
} else if let uint32 = try? container.decode(UInt32.self) {
|
||||
self.value = uint32
|
||||
} else if let uint64 = try? container.decode(UInt64.self) {
|
||||
self.value = uint64
|
||||
} else if let double = try? container.decode(Double.self) {
|
||||
self.value = double
|
||||
} else if let float = try? container.decode(Float.self) {
|
||||
self.value = float
|
||||
} 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()
|
||||
switch value {
|
||||
case is Void:
|
||||
try container.encodeNil()
|
||||
case let bool as Bool:
|
||||
try container.encode(bool)
|
||||
case let int as Int:
|
||||
try container.encode(int)
|
||||
case let int32 as Int32:
|
||||
try container.encode(Int(int32))
|
||||
case let int64 as Int64:
|
||||
try container.encode(int64)
|
||||
case let uint as UInt:
|
||||
try container.encode(uint)
|
||||
case let uint32 as UInt32:
|
||||
try container.encode(uint32)
|
||||
case let uint64 as UInt64:
|
||||
try container.encode(uint64)
|
||||
case let double as Double:
|
||||
try container.encode(double)
|
||||
case let float as Float:
|
||||
try container.encode(float)
|
||||
case let string as String:
|
||||
try container.encode(string)
|
||||
case let array as [AnyCodable]:
|
||||
try container.encode(array)
|
||||
case let array as [Any?]:
|
||||
try container.encode(array.map { AnyCodable($0) })
|
||||
case let dictionary as [String: AnyCodable]:
|
||||
try container.encode(dictionary)
|
||||
case let dictionary as [String: Any?]:
|
||||
try container.encode(dictionary.mapValues { AnyCodable($0) })
|
||||
default:
|
||||
let context = EncodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "AnyCodable value cannot be encoded"
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,82 +18,82 @@ public struct Attribute<T> {
|
||||
}
|
||||
|
||||
// MARK: - General Element Attributes
|
||||
public static var role: Attribute<String> { Attribute<String>(kAXRoleAttribute) }
|
||||
public static var subrole: Attribute<String> { Attribute<String>(kAXSubroleAttribute) }
|
||||
public static var roleDescription: Attribute<String> { Attribute<String>(kAXRoleDescriptionAttribute) }
|
||||
public static var title: Attribute<String> { Attribute<String>(kAXTitleAttribute) }
|
||||
public static var description: Attribute<String> { Attribute<String>(kAXDescriptionAttribute) }
|
||||
public static var help: Attribute<String> { Attribute<String>(kAXHelpAttribute) }
|
||||
public static var identifier: Attribute<String> { Attribute<String>(kAXIdentifierAttribute) }
|
||||
public static var role: Attribute<String> { Attribute<String>(AXAttributeNames.kAXRoleAttribute) }
|
||||
public static var subrole: Attribute<String> { Attribute<String>(AXAttributeNames.kAXSubroleAttribute) }
|
||||
public static var roleDescription: Attribute<String> { Attribute<String>(AXAttributeNames.kAXRoleDescriptionAttribute) }
|
||||
public static var title: Attribute<String> { Attribute<String>(AXAttributeNames.kAXTitleAttribute) }
|
||||
public static var description: Attribute<String> { Attribute<String>(AXAttributeNames.kAXDescriptionAttribute) }
|
||||
public static var help: Attribute<String> { Attribute<String>(AXAttributeNames.kAXHelpAttribute) }
|
||||
public static var identifier: Attribute<String> { Attribute<String>(AXAttributeNames.kAXIdentifierAttribute) }
|
||||
|
||||
// MARK: - Value Attributes
|
||||
// kAXValueAttribute can be many types. For a generic getter, Any might be appropriate,
|
||||
// or specific versions if the context knows the type.
|
||||
public static var value: Attribute<Any> { Attribute<Any>(kAXValueAttribute) }
|
||||
public static var value: Attribute<Any> { Attribute<Any>(AXAttributeNames.kAXValueAttribute) }
|
||||
// Example of a more specific value if known:
|
||||
// static var stringValue: Attribute<String> { Attribute(kAXValueAttribute) }
|
||||
|
||||
// MARK: - State Attributes
|
||||
public static var enabled: Attribute<Bool> { Attribute<Bool>(kAXEnabledAttribute) }
|
||||
public static var focused: Attribute<Bool> { Attribute<Bool>(kAXFocusedAttribute) }
|
||||
public static var busy: Attribute<Bool> { Attribute<Bool>(kAXElementBusyAttribute) }
|
||||
public static var hidden: Attribute<Bool> { Attribute<Bool>(kAXHiddenAttribute) }
|
||||
public static var enabled: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXEnabledAttribute) }
|
||||
public static var focused: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXFocusedAttribute) }
|
||||
public static var busy: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXElementBusyAttribute) }
|
||||
public static var hidden: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXHiddenAttribute) }
|
||||
|
||||
// MARK: - Hierarchy Attributes
|
||||
public static var parent: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXParentAttribute) }
|
||||
public static var parent: Attribute<AXUIElement> { Attribute<AXUIElement>(AXAttributeNames.kAXParentAttribute) }
|
||||
// For children, the direct attribute often returns [AXUIElement].
|
||||
// Element.children getter then wraps these.
|
||||
public static var children: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXChildrenAttribute) }
|
||||
public static var children: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXChildrenAttribute) }
|
||||
public static var selectedChildren: Attribute<[AXUIElement]> {
|
||||
Attribute<[AXUIElement]>(kAXSelectedChildrenAttribute) }
|
||||
public static var visibleChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleChildrenAttribute)
|
||||
Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedChildrenAttribute) }
|
||||
public static var visibleChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXVisibleChildrenAttribute)
|
||||
}
|
||||
public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXWindowsAttribute) }
|
||||
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMainWindowAttribute)
|
||||
public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXWindowsAttribute) }
|
||||
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXMainWindowAttribute)
|
||||
} // Can be nil
|
||||
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedWindowAttribute)
|
||||
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFocusedWindowAttribute)
|
||||
} // Can be nil
|
||||
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedUIElementAttribute)
|
||||
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFocusedUIElementAttribute)
|
||||
} // Can be nil
|
||||
|
||||
// MARK: - Application Specific Attributes
|
||||
// public static var enhancedUserInterface: Attribute<Bool> { Attribute<Bool>(kAXEnhancedUserInterfaceAttribute) } // Constant not found, commenting out
|
||||
public static var frontmost: Attribute<Bool> { Attribute<Bool>(kAXFrontmostAttribute) }
|
||||
public static var mainMenu: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXMenuBarAttribute) }
|
||||
public static var frontmost: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXFrontmostAttribute) }
|
||||
public static var mainMenu: Attribute<AXUIElement> { Attribute<AXUIElement>(AXAttributeNames.kAXMenuBarAttribute) }
|
||||
// public static var hiddenApplication: Attribute<Bool> { Attribute(kAXHiddenAttribute) } // Same as element hidden, but for app. Covered by .hidden
|
||||
|
||||
// MARK: - Window Specific Attributes
|
||||
public static var minimized: Attribute<Bool> { Attribute<Bool>(kAXMinimizedAttribute) }
|
||||
public static var modal: Attribute<Bool> { Attribute<Bool>(kAXModalAttribute) }
|
||||
public static var defaultButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXDefaultButtonAttribute) }
|
||||
public static var cancelButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCancelButtonAttribute) }
|
||||
public static var closeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCloseButtonAttribute) }
|
||||
public static var zoomButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXZoomButtonAttribute) }
|
||||
public static var minimizeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMinimizeButtonAttribute) }
|
||||
public static var toolbarButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXToolbarButtonAttribute) }
|
||||
public static var fullScreenButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFullScreenButtonAttribute)
|
||||
public static var minimized: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXMinimizedAttribute) }
|
||||
public static var modal: Attribute<Bool> { Attribute<Bool>(AXAttributeNames.kAXModalAttribute) }
|
||||
public static var defaultButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXDefaultButtonAttribute) }
|
||||
public static var cancelButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXCancelButtonAttribute) }
|
||||
public static var closeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXCloseButtonAttribute) }
|
||||
public static var zoomButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXZoomButtonAttribute) }
|
||||
public static var minimizeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXMinimizeButtonAttribute) }
|
||||
public static var toolbarButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXToolbarButtonAttribute) }
|
||||
public static var fullScreenButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXFullScreenButtonAttribute)
|
||||
}
|
||||
public static var proxy: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXProxyAttribute) }
|
||||
public static var growArea: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXGrowAreaAttribute) }
|
||||
public static var proxy: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXProxyAttribute) }
|
||||
public static var growArea: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXGrowAreaAttribute) }
|
||||
|
||||
// MARK: - Table/List/Outline Attributes
|
||||
public static var rows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXRowsAttribute) }
|
||||
public static var columns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXColumnsAttribute) }
|
||||
public static var selectedRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedRowsAttribute) }
|
||||
public static var selectedColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedColumnsAttribute)
|
||||
public static var rows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXRowsAttribute) }
|
||||
public static var columns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXColumnsAttribute) }
|
||||
public static var selectedRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedRowsAttribute) }
|
||||
public static var selectedColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedColumnsAttribute)
|
||||
}
|
||||
public static var selectedCells: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedCellsAttribute) }
|
||||
public static var selectedCells: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(AXAttributeNames.kAXSelectedCellsAttribute) }
|
||||
public static var visibleRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleRowsAttribute) }
|
||||
public static var visibleColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleColumnsAttribute) }
|
||||
public static var header: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXHeaderAttribute) }
|
||||
public static var header: Attribute<AXUIElement?> { Attribute<AXUIElement?>(AXAttributeNames.kAXHeaderAttribute) }
|
||||
public static var orientation: Attribute<String> { Attribute<String>(kAXOrientationAttribute)
|
||||
} // e.g., kAXVerticalOrientationValue
|
||||
|
||||
// MARK: - Text Attributes
|
||||
public static var selectedText: Attribute<String> { Attribute<String>(kAXSelectedTextAttribute) }
|
||||
public static var selectedTextRange: Attribute<CFRange> { Attribute<CFRange>(kAXSelectedTextRangeAttribute) }
|
||||
public static var numberOfCharacters: Attribute<Int> { Attribute<Int>(kAXNumberOfCharactersAttribute) }
|
||||
public static var visibleCharacterRange: Attribute<CFRange> { Attribute<CFRange>(kAXVisibleCharacterRangeAttribute)
|
||||
public static var selectedText: Attribute<String> { Attribute<String>(AXAttributeNames.kAXSelectedTextAttribute) }
|
||||
public static var selectedTextRange: Attribute<CFRange> { Attribute<CFRange>(AXAttributeNames.kAXSelectedTextRangeAttribute) }
|
||||
public static var numberOfCharacters: Attribute<Int> { Attribute<Int>(AXAttributeNames.kAXNumberOfCharactersAttribute) }
|
||||
public static var visibleCharacterRange: Attribute<CFRange> { Attribute<CFRange>(AXAttributeNames.kAXVisibleCharacterRangeAttribute)
|
||||
}
|
||||
// Parameterized attributes are handled differently, often via functions.
|
||||
// static var attributedStringForRange: Attribute<NSAttributedString> { Attribute(kAXAttributedStringForRangeParameterizedAttribute) }
|
||||
@ -101,22 +101,22 @@ public struct Attribute<T> {
|
||||
|
||||
// MARK: - Scroll Area Attributes
|
||||
public static var horizontalScrollBar: Attribute<AXUIElement?> {
|
||||
Attribute<AXUIElement?>(kAXHorizontalScrollBarAttribute) }
|
||||
Attribute<AXUIElement?>(AXAttributeNames.kAXHorizontalScrollBarAttribute) }
|
||||
public static var verticalScrollBar: Attribute<AXUIElement?> {
|
||||
Attribute<AXUIElement?>(kAXVerticalScrollBarAttribute) }
|
||||
Attribute<AXUIElement?>(AXAttributeNames.kAXVerticalScrollBarAttribute) }
|
||||
|
||||
// MARK: - Action Related
|
||||
// Action names are typically an array of strings.
|
||||
public static var actionNames: Attribute<[String]> { Attribute<[String]>(kAXActionNamesAttribute) }
|
||||
public static var actionNames: Attribute<[String]> { Attribute<[String]>(AXAttributeNames.kAXActionNamesAttribute) }
|
||||
// Action description is parameterized by the action name, so a simple Attribute<String> isn't quite right.
|
||||
// It would be kAXActionDescriptionAttribute, and you pass a parameter.
|
||||
// For now, we will represent it as taking a string, and the usage site will need to handle parameterization.
|
||||
public static var actionDescription: Attribute<String> { Attribute<String>(kAXActionDescriptionAttribute) }
|
||||
public static var actionDescription: Attribute<String> { Attribute<String>(AXAttributeNames.kAXActionDescriptionAttribute) }
|
||||
|
||||
// MARK: - AXValue holding attributes (expect these to return AXValueRef)
|
||||
// These will typically be unwrapped by a helper function (like ValueParser or similar) into their Swift types.
|
||||
public static var position: Attribute<CGPoint> { Attribute<CGPoint>(kAXPositionAttribute) }
|
||||
public static var size: Attribute<CGSize> { Attribute<CGSize>(kAXSizeAttribute) }
|
||||
public static var position: Attribute<CGPoint> { Attribute<CGPoint>(AXAttributeNames.kAXPositionAttribute) }
|
||||
public static var size: Attribute<CGSize> { Attribute<CGSize>(AXAttributeNames.kAXSizeAttribute) }
|
||||
// Note: CGRect for kAXBoundsAttribute is also common if available.
|
||||
// For now, relying on position and size.
|
||||
|
||||
|
||||
75
Sources/AXorcist/Core/CommandModels.swift
Normal file
75
Sources/AXorcist/Core/CommandModels.swift
Normal file
@ -0,0 +1,75 @@
|
||||
// CommandModels.swift - Contains command-related model structs
|
||||
|
||||
import Foundation
|
||||
|
||||
// Main command envelope - REPLACED with definition from axorc.swift for consistency
|
||||
public struct CommandEnvelope: Codable {
|
||||
public let command_id: 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 debug_logging: Bool?
|
||||
public let locator: Locator? // Locator from this file
|
||||
public let path_hint: [String]?
|
||||
public let max_elements: Int?
|
||||
public let output_format: OutputFormat? // OutputFormat from this file
|
||||
public let action_name: String? // For performAction
|
||||
public let action_value: AnyCodable? // For performAction (AnyCodable from this file)
|
||||
public let sub_commands: [CommandEnvelope]? // For batch command
|
||||
|
||||
// Added a public initializer for convenience, matching fields.
|
||||
public init(command_id: String,
|
||||
command: CommandType,
|
||||
application: String? = nil,
|
||||
attributes: [String]? = nil,
|
||||
payload: [String: String]? = nil,
|
||||
debug_logging: Bool? = nil,
|
||||
locator: Locator? = nil,
|
||||
path_hint: [String]? = nil,
|
||||
max_elements: Int? = nil,
|
||||
output_format: OutputFormat? = nil,
|
||||
action_name: String? = nil,
|
||||
action_value: AnyCodable? = nil,
|
||||
sub_commands: [CommandEnvelope]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.command = command
|
||||
self.application = application
|
||||
self.attributes = attributes
|
||||
self.payload = payload
|
||||
self.debug_logging = debug_logging
|
||||
self.locator = locator
|
||||
self.path_hint = path_hint
|
||||
self.max_elements = max_elements
|
||||
self.output_format = output_format
|
||||
self.action_name = action_name
|
||||
self.action_value = action_value
|
||||
self.sub_commands = sub_commands
|
||||
}
|
||||
}
|
||||
|
||||
// Locator for finding elements
|
||||
public struct Locator: Codable {
|
||||
public var match_all: Bool?
|
||||
public var criteria: [String: String]
|
||||
public var root_element_path_hint: [String]?
|
||||
public var requireAction: String?
|
||||
public var computed_name_contains: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case match_all
|
||||
case criteria
|
||||
case root_element_path_hint
|
||||
case requireAction = "require_action"
|
||||
case computed_name_contains
|
||||
}
|
||||
|
||||
public init(match_all: Bool? = nil, criteria: [String: String] = [:], root_element_path_hint: [String]? = nil,
|
||||
requireAction: String? = nil, computed_name_contains: String? = nil) {
|
||||
self.match_all = match_all
|
||||
self.criteria = criteria
|
||||
self.root_element_path_hint = root_element_path_hint
|
||||
self.requireAction = requireAction
|
||||
self.computed_name_contains = computed_name_contains
|
||||
}
|
||||
}
|
||||
40
Sources/AXorcist/Core/DataModels.swift
Normal file
40
Sources/AXorcist/Core/DataModels.swift
Normal file
@ -0,0 +1,40 @@
|
||||
// Models.swift - Contains core data models and type aliases
|
||||
|
||||
import Foundation
|
||||
|
||||
// Type alias for element attributes dictionary
|
||||
public typealias ElementAttributes = [String: AnyCodable]
|
||||
|
||||
public struct AXElement: Codable {
|
||||
public var attributes: ElementAttributes?
|
||||
public var path: [String]?
|
||||
|
||||
public init(attributes: ElementAttributes?, path: [String]? = nil) {
|
||||
self.attributes = attributes
|
||||
self.path = path
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Search Log Entry Model (for stderr JSON logging)
|
||||
public struct SearchLogEntry: Codable {
|
||||
public let d: Int // depth
|
||||
public let eR: String? // elementRole
|
||||
public let eT: String? // elementTitle
|
||||
public let eI: String? // elementIdentifier
|
||||
public let mD: Int // maxDepth
|
||||
public let c: [String: String]? // criteria (abbreviated)
|
||||
public let s: String // status (e.g., "vis", "found", "noMatch", "maxD")
|
||||
public let iM: Bool? // isMatch (true, false, or nil if not applicable for this status)
|
||||
|
||||
// Public initializer
|
||||
public init(d: Int, eR: String?, eT: String?, eI: String?, mD: Int, c: [String: String]?, s: String, iM: Bool?) {
|
||||
self.d = d
|
||||
self.eR = eR
|
||||
self.eT = eT
|
||||
self.eI = eI
|
||||
self.mD = mD
|
||||
self.c = c
|
||||
self.s = s
|
||||
self.iM = iM
|
||||
}
|
||||
}
|
||||
73
Sources/AXorcist/Core/Element+Actions.swift
Normal file
73
Sources/AXorcist/Core/Element+Actions.swift
Normal file
@ -0,0 +1,73 @@
|
||||
// Element+Actions.swift - Action-related methods for Element
|
||||
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Action-related extension for Element
|
||||
extension Element {
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@MainActor
|
||||
public func isActionSupported(_ actionName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> Bool {
|
||||
// dLog is not directly used here, logging comes from the attribute call
|
||||
if let actions: [String] = attribute(
|
||||
Attribute<[String]>.actionNames,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs // This will respect the new dLog behavior in .attribute()
|
||||
) {
|
||||
return actions.contains(actionName)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@discardableResult
|
||||
public func performAction(
|
||||
_ actionName: Attribute<String>,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) throws -> Element {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
let error = AXUIElementPerformAction(self.underlyingElement, actionName.rawValue as CFString)
|
||||
if error != .success {
|
||||
// Now call the refactored briefDescription, passing the logs along.
|
||||
let desc = self.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog("Action \(actionName.rawValue) failed on element \(desc). Error: \(error.rawValue)")
|
||||
throw AccessibilityError.actionFailed("Action \(actionName.rawValue) failed on element \(desc)", error)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@discardableResult
|
||||
public func performAction(_ actionName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) throws -> Element {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
let error = AXUIElementPerformAction(self.underlyingElement, actionName as CFString)
|
||||
if error != .success {
|
||||
// Now call the refactored briefDescription, passing the logs along.
|
||||
let desc = self.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)")
|
||||
throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
85
Sources/AXorcist/Core/Element+Description.swift
Normal file
85
Sources/AXorcist/Core/Element+Description.swift
Normal file
@ -0,0 +1,85 @@
|
||||
// Element+Description.swift - Extension for Element description functionality
|
||||
|
||||
import ApplicationServices // For AXUIElement and other C APIs
|
||||
import Foundation
|
||||
|
||||
// MARK: - Element Description Extension
|
||||
|
||||
extension Element {
|
||||
@MainActor
|
||||
public func briefDescription(option: ValueFormatOption = .default, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
|
||||
var descriptionParts: [String] = []
|
||||
var tempLogs: [String] = []
|
||||
|
||||
if let role = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
descriptionParts.append("Role: \(role)")
|
||||
}
|
||||
|
||||
// PID, Title, ID, DOMId for .default and .verbose
|
||||
if option == .default || option == .verbose {
|
||||
var pidLogs: [String] = []
|
||||
if let pidValue = self.pid(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &pidLogs) {
|
||||
descriptionParts.append("PID: \(pidValue)")
|
||||
}
|
||||
if isDebugLoggingEnabled && false { currentDebugLogs.append(contentsOf: pidLogs) }
|
||||
|
||||
if let title = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
if !title.isEmpty {
|
||||
descriptionParts.append("Title: '\(title.truncated(to: 50))'")
|
||||
}
|
||||
}
|
||||
|
||||
if let id = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
if !id.isEmpty {
|
||||
descriptionParts.append("ID: '\(id.truncated(to: 50))'")
|
||||
}
|
||||
}
|
||||
|
||||
if let domId = self.domIdentifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
if !domId.isEmpty {
|
||||
descriptionParts.append("DOMId: '\(domId.truncated(to: 50))'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Value and Help for .verbose only
|
||||
if option == .verbose {
|
||||
if let value = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
let valueStr = String(describing: value)
|
||||
if !valueStr.isEmpty && valueStr != "nil" {
|
||||
descriptionParts.append("Value: '\(valueStr.truncated(to: 80))'")
|
||||
}
|
||||
}
|
||||
|
||||
if let help = self.help(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
if !help.isEmpty {
|
||||
descriptionParts.append("Help: '\(help.truncated(to: 80))'")
|
||||
}
|
||||
}
|
||||
}
|
||||
// For .short, only Role is included (implicitly from the first lines)
|
||||
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
}
|
||||
|
||||
if descriptionParts.isEmpty {
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("briefDescription: No descriptive attributes found, falling back to underlyingElement description.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
return String(describing: self.underlyingElement)
|
||||
}
|
||||
|
||||
// If .short and we have more than just Role (or Role+PID if PID was included for default), then shorten further.
|
||||
// This logic might need refinement based on desired .short output.
|
||||
if option == .short && descriptionParts.count > 1 {
|
||||
if let role = self.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempLogs) { // Get role again without logging
|
||||
return "Role: \(role)" // Just return role for short
|
||||
} else {
|
||||
return descriptionParts.first ?? String(describing: self.underlyingElement) // Fallback for short
|
||||
}
|
||||
}
|
||||
|
||||
return descriptionParts.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,42 @@
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// MARK: - Environment Variable Check for JSON Logging
|
||||
// Copied from ElementSearch.swift - ideally this would be in a shared utility
|
||||
private func getEnvVar(_ name: String) -> String? {
|
||||
guard let value = getenv(name) else { return nil }
|
||||
return String(cString: value)
|
||||
}
|
||||
|
||||
private let AXORC_JSON_LOG_ENABLED: Bool = {
|
||||
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
|
||||
// Explicitly log the check to stderr for debugging the env var itself, specific to Element+Hierarchy.swift
|
||||
fputs("[Element+Hierarchy.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
|
||||
return envValue == "true"
|
||||
}()
|
||||
|
||||
// MARK: - Element Hierarchy Logic
|
||||
|
||||
extension Element {
|
||||
@MainActor
|
||||
public func children(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } }
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
let elementDescription = self.briefDescription(
|
||||
let elementDescriptionForLog = self.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog("Getting children for element: \(elementDescription)")
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("Getting children for element: \(elementDescriptionForLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
|
||||
var childCollector = ChildCollector()
|
||||
|
||||
// Collect children from various sources
|
||||
collectDirectChildren(
|
||||
collector: &childCollector,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
@ -36,8 +55,15 @@ extension Element {
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
let result = childCollector.finalizeResults(dLog: dLog)
|
||||
dLog("Final children count from Element.children: \(result?.count ?? 0)")
|
||||
let result = childCollector.finalizeResults(dLog: { message in
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
})
|
||||
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("Final children count from Element.children: \(result?.count ?? 0)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -47,37 +73,45 @@ extension Element {
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) {
|
||||
// DO NOT CALL self.briefDescription() or self.attribute() BEFORE THE kAXChildrenAttribute CALL BELOW
|
||||
// Log entry for this function can be done by the caller (Element.children) if needed before this is called,
|
||||
// or log a generic message here without using self.briefDescription().
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren: Attempting to fetch kAXChildrenAttribute directly.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren: Attempting to fetch kAXChildrenAttribute directly.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
|
||||
var value: CFTypeRef?
|
||||
let error = AXUIElementCopyAttributeValue(self.underlyingElement, kAXChildrenAttribute as CFString, &value)
|
||||
let error = AXUIElementCopyAttributeValue(self.underlyingElement, AXAttributeNames.kAXChildrenAttribute as CFString, &value)
|
||||
|
||||
// It's safer to get description AFTER the critical kAXChildrenAttribute call
|
||||
let selfDescForLog = isDebugLoggingEnabled ? self.briefDescription(option: .short, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) : "Element(debug_off)"
|
||||
let selfDescForLog = (isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED) ? self.briefDescription(option: .short, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) : "Element(json_log_on_or_debug_off)"
|
||||
|
||||
if error == .success {
|
||||
if let childrenCFArray = value, CFGetTypeID(childrenCFArray) == CFArrayGetTypeID() {
|
||||
if let directChildrenUI = childrenCFArray as? [AXUIElement] {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Successfully fetched and cast \(directChildrenUI.count) direct children.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Successfully fetched and cast \(directChildrenUI.count) direct children.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
collector.addChildren(from: directChildrenUI)
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was a CFArray but failed to cast to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was a CFArray but failed to cast to [AXUIElement]. TypeID: \(CFGetTypeID(childrenCFArray))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
} else if let nonArrayValue = value {
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was not a CFArray. TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
} else if let nonArrayValue = value {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was not a CFArray. TypeID: \(CFGetTypeID(nonArrayValue)). Value: \(String(describing: nonArrayValue))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was nil despite .success error code.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute was nil despite .success error code.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
} else if error == .noValue {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute has no value.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: kAXChildrenAttribute has no value.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Error fetching kAXChildrenAttribute: \(error.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectDirectChildren [\(selfDescForLog)]: Error fetching kAXChildrenAttribute: \(error.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
// No CFRelease(value) needed here if childrenCFArray was an array, as `as? [AXUIElement]` handles bridging.
|
||||
// If it was nonArrayValue, it's a bit more ambiguous but usually these are also bridged or not needing manual release for simple gets.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -87,14 +121,16 @@ extension Element {
|
||||
currentDebugLogs: inout [String]
|
||||
) {
|
||||
let alternativeAttributes: [String] = [
|
||||
kAXVisibleChildrenAttribute, kAXWebAreaChildrenAttribute, kAXHTMLContentAttribute,
|
||||
kAXARIADOMChildrenAttribute, kAXDOMChildrenAttribute, kAXApplicationNavigationAttribute,
|
||||
kAXApplicationElementsAttribute, kAXContentsAttribute, kAXBodyAreaAttribute, kAXDocumentContentAttribute,
|
||||
kAXWebPageContentAttribute, kAXSplitGroupContentsAttribute, kAXLayoutAreaChildrenAttribute,
|
||||
kAXGroupChildrenAttribute, kAXSelectedChildrenAttribute, kAXRowsAttribute, kAXColumnsAttribute,
|
||||
kAXTabsAttribute
|
||||
AXAttributeNames.kAXVisibleChildrenAttribute, AXAttributeNames.kAXWebAreaChildrenAttribute, AXAttributeNames.kAXHTMLContentAttribute,
|
||||
AXAttributeNames.kAXARIADOMChildrenAttribute, AXAttributeNames.kAXDOMChildrenAttribute, AXAttributeNames.kAXApplicationNavigationAttribute,
|
||||
AXAttributeNames.kAXApplicationElementsAttribute, AXAttributeNames.kAXContentsAttribute, AXAttributeNames.kAXBodyAreaAttribute, AXAttributeNames.kAXDocumentContentAttribute,
|
||||
AXAttributeNames.kAXWebPageContentAttribute, AXAttributeNames.kAXSplitGroupContentsAttribute, AXAttributeNames.kAXLayoutAreaChildrenAttribute,
|
||||
AXAttributeNames.kAXGroupChildrenAttribute, AXAttributeNames.kAXSelectedChildrenAttribute, AXAttributeNames.kAXRowsAttribute, AXAttributeNames.kAXColumnsAttribute,
|
||||
AXAttributeNames.kAXTabsAttribute
|
||||
]
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectAlternativeChildren: Will iterate \(alternativeAttributes.count) alternative attributes.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectAlternativeChildren: Will iterate \(alternativeAttributes.count) alternative attributes.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
|
||||
for attrName in alternativeAttributes {
|
||||
collectChildrenFromAttribute(
|
||||
@ -113,24 +149,36 @@ extension Element {
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) {
|
||||
var tempLogs: [String] = []
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Trying '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
var tempLogs: [String] = [] // attribute() method logs to this, and it respects AXORC_JSON_LOG_ENABLED internally
|
||||
|
||||
// This initial log for the function call itself needs to be conditional
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Trying '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
|
||||
if let childrenUI: [AXUIElement] = attribute(
|
||||
Attribute<[AXUIElement]>(attributeName),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
currentDebugLogs: &tempLogs // attribute() logs here conditionally
|
||||
) {
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
// Append tempLogs to currentDebugLogs *only if* they would have been logged by attribute() anyway
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogs) }
|
||||
|
||||
if !childrenUI.isEmpty {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Successfully fetched \(childrenUI.count) children from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Successfully fetched \(childrenUI.count) children from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
collector.addChildren(from: childrenUI)
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Fetched EMPTY array from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Fetched EMPTY array from '\(attributeName)'.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Attribute '\(attributeName)' returned nil or was not [AXUIElement].", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogs) }
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectChildrenFromAttribute: Attribute '\(attributeName)' returned nil or was not [AXUIElement].", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,26 +190,35 @@ extension Element {
|
||||
) {
|
||||
var tempLogsForRole: [String] = []
|
||||
let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForRole)
|
||||
currentDebugLogs.append(contentsOf: tempLogsForRole)
|
||||
// Append role logs only if general debug logging is on and JSON is off
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForRole) }
|
||||
|
||||
if currentRole == kAXApplicationRole as String {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Element is AXApplication. Trying kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if currentRole == AXRoleNames.kAXApplicationRole as String {
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Element is AXApplication. Trying kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
var tempLogsForWindows: [String] = []
|
||||
if let windowElementsUI: [AXUIElement] = attribute(
|
||||
Attribute<[AXUIElement]>.windows,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForWindows
|
||||
) {
|
||||
currentDebugLogs.append(contentsOf: tempLogsForWindows)
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForWindows) }
|
||||
if !windowElementsUI.isEmpty {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Successfully fetched \(windowElementsUI.count) windows.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Successfully fetched \(windowElementsUI.count) windows.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
collector.addChildren(from: windowElementsUI)
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Fetched EMPTY array from kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Fetched EMPTY array from kAXWindowsAttribute.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentDebugLogs.append(contentsOf: tempLogsForWindows)
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Attribute kAXWindowsAttribute returned nil.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED { currentDebugLogs.append(contentsOf: tempLogsForWindows) }
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("collectApplicationWindows: Attribute kAXWindowsAttribute returned nil.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,14 +236,13 @@ private struct ChildCollector {
|
||||
for childUI in childrenUI {
|
||||
let childElement = Element(childUI)
|
||||
if !uniqueChildrenSet.contains(childElement) {
|
||||
// Log before adding
|
||||
// AXorcist.formatDebugLogMessage("ChildCollector: Adding new child: \(childElement.briefDescription(option: .minimal))", ... ) - too verbose for now
|
||||
collectedChildren.append(childElement)
|
||||
uniqueChildrenSet.insert(childElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dLog is now a closure passed in, which should itself be conditional
|
||||
func finalizeResults(dLog: (String) -> Void) -> [Element]? {
|
||||
if collectedChildren.isEmpty {
|
||||
dLog("ChildCollector.finalizeResults: No children found for element after all collection methods.")
|
||||
|
||||
80
Sources/AXorcist/Core/Element+ParameterizedAttributes.swift
Normal file
80
Sources/AXorcist/Core/Element+ParameterizedAttributes.swift
Normal file
@ -0,0 +1,80 @@
|
||||
// Element+ParameterizedAttributes.swift - Extension for parameterized attribute functionality
|
||||
|
||||
import ApplicationServices // For AXUIElement and other C APIs
|
||||
import Foundation
|
||||
|
||||
// MARK: - Parameterized Attributes Extension
|
||||
extension Element {
|
||||
@MainActor
|
||||
public func parameterizedAttribute<T>(
|
||||
_ attribute: Attribute<T>,
|
||||
forParameter parameter: Any,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> T? {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
var cfParameter: CFTypeRef?
|
||||
|
||||
// Convert Swift parameter to CFTypeRef for the API
|
||||
if var range = parameter as? CFRange {
|
||||
cfParameter = AXValueCreate(.cfRange, &range)
|
||||
} else if let string = parameter as? String {
|
||||
cfParameter = string as CFString
|
||||
} else if let number = parameter as? NSNumber {
|
||||
cfParameter = number
|
||||
} else if CFGetTypeID(parameter as CFTypeRef) != 0 { // Check if it's already a CFTypeRef-compatible type
|
||||
cfParameter = (parameter as CFTypeRef)
|
||||
} else {
|
||||
dLog("parameterizedAttribute: Unsupported parameter type \(type(of: parameter))")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let actualCFParameter = cfParameter else {
|
||||
dLog("parameterizedAttribute: Failed to convert parameter to CFTypeRef.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var value: CFTypeRef?
|
||||
let error = AXUIElementCopyParameterizedAttributeValue(
|
||||
underlyingElement,
|
||||
attribute.rawValue as CFString,
|
||||
actualCFParameter,
|
||||
&value
|
||||
)
|
||||
|
||||
if error != .success {
|
||||
dLog("parameterizedAttribute: Error \(error.rawValue) getting attribute \(attribute.rawValue)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let resultCFValue = value else { return nil }
|
||||
|
||||
// Use axValue's unwrapping and casting logic if possible, by temporarily creating an element and attribute
|
||||
// This is a bit of a conceptual stretch, as axValue is designed for direct attributes.
|
||||
// A more direct unwrap using ValueUnwrapper might be cleaner here.
|
||||
let unwrappedValue = ValueUnwrapper.unwrap(
|
||||
resultCFValue,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
guard let finalValue = unwrappedValue else { return nil }
|
||||
|
||||
// Perform type casting similar to axValue
|
||||
if T.self == String.self {
|
||||
if let str = finalValue as? String { return str as? T } else if let attrStr = finalValue as? NSAttributedString { return attrStr.string as? T }
|
||||
return nil
|
||||
}
|
||||
if let castedValue = finalValue as? T {
|
||||
return castedValue
|
||||
}
|
||||
dLog(
|
||||
"parameterizedAttribute: Fallback cast attempt for attribute '\(attribute.rawValue)' to type \(T.self) FAILED. Unwrapped value was \(type(of: finalValue)): \(finalValue)"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
169
Sources/AXorcist/Core/Element+PathGeneration.swift
Normal file
169
Sources/AXorcist/Core/Element+PathGeneration.swift
Normal file
@ -0,0 +1,169 @@
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Extension to generate a descriptive path string
|
||||
extension Element {
|
||||
@MainActor
|
||||
// Update signature to include logging parameters
|
||||
public func generatePathString(
|
||||
upTo ancestor: Element? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> String {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
var pathComponents: [String] = []
|
||||
var currentElement: Element? = self
|
||||
|
||||
var depth = 0 // Safety break for very deep or circular hierarchies
|
||||
let maxDepth = 25
|
||||
var tempLogs: [String] = [] // Temporary logs for calls within the loop
|
||||
|
||||
dLog(
|
||||
"generatePathString started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
|
||||
)
|
||||
|
||||
while let element = currentElement, depth < maxDepth {
|
||||
tempLogs.removeAll() // Clear for each iteration
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
pathComponents.append(briefDesc)
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Append logs from briefDescription
|
||||
|
||||
if let ancestor = ancestor, element == ancestor {
|
||||
dLog("generatePathString: Reached specified ancestor: \(briefDesc)")
|
||||
break // Reached the specified ancestor
|
||||
}
|
||||
|
||||
// Check role to prevent going above application or a window if its parent is the app
|
||||
tempLogs.removeAll()
|
||||
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentElement = element.parent(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentRole = parentElement?.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
if role == AXRoleNames.kAXApplicationRole ||
|
||||
(role == AXRoleNames.kAXWindowRole && parentRole == AXRoleNames.kAXApplicationRole && ancestor == nil) {
|
||||
dLog(
|
||||
"generatePathString: Stopping at \(role == AXRoleNames.kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
currentElement = parentElement
|
||||
depth += 1
|
||||
if currentElement == nil && role != AXRoleNames.kAXApplicationRole {
|
||||
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
|
||||
dLog("generatePathString: Unexpected orphan: \(orphanLog)")
|
||||
pathComponents.append(orphanLog)
|
||||
break
|
||||
}
|
||||
}
|
||||
if depth >= maxDepth {
|
||||
dLog("generatePathString: Reached max depth (\(maxDepth)). Path might be truncated.")
|
||||
pathComponents.append("<...max_depth_reached...>")
|
||||
}
|
||||
|
||||
let finalPath = pathComponents.reversed().joined(separator: " -> ")
|
||||
dLog("generatePathString finished. Path: \(finalPath)")
|
||||
return finalPath
|
||||
}
|
||||
|
||||
// New function to return path components as an array
|
||||
@MainActor
|
||||
public func generatePathArray(
|
||||
upTo ancestor: Element? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> [String] {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled && false { currentDebugLogs.append(message) } }
|
||||
var pathComponents: [String] = []
|
||||
var currentElement: Element? = self
|
||||
|
||||
var depth = 0
|
||||
let maxDepth = 25
|
||||
var tempLogs: [String] = []
|
||||
|
||||
dLog(
|
||||
"generatePathArray started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
while let element = currentElement, depth < maxDepth {
|
||||
tempLogs.removeAll()
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
pathComponents.append(briefDesc)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
if let ancestor = ancestor, element == ancestor {
|
||||
dLog("generatePathArray: Reached specified ancestor: \(briefDesc)")
|
||||
break
|
||||
}
|
||||
|
||||
tempLogs.removeAll()
|
||||
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentElement = element.parent(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentRole = parentElement?.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
if role == AXRoleNames.kAXApplicationRole ||
|
||||
(role == AXRoleNames.kAXWindowRole && parentRole == AXRoleNames.kAXApplicationRole && ancestor == nil) {
|
||||
dLog(
|
||||
"generatePathArray: Stopping at \(role == AXRoleNames.kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
currentElement = parentElement
|
||||
depth += 1
|
||||
if currentElement == nil && role != AXRoleNames.kAXApplicationRole {
|
||||
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
|
||||
dLog("generatePathArray: Unexpected orphan: \(orphanLog)")
|
||||
pathComponents.append(orphanLog)
|
||||
break
|
||||
}
|
||||
}
|
||||
if depth >= maxDepth {
|
||||
dLog("generatePathArray: Reached max depth (\(maxDepth)). Path might be truncated.")
|
||||
pathComponents.append("<...max_depth_reached...>")
|
||||
}
|
||||
|
||||
let reversedPathComponents = Array(pathComponents.reversed())
|
||||
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
|
||||
return reversedPathComponents
|
||||
}
|
||||
}
|
||||
@ -172,24 +172,14 @@ extension Element {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func briefDescription(
|
||||
option: ValueFormatOption = .default,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> String {
|
||||
let role = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "Unknown"
|
||||
let title = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
let identifier = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
var description = role
|
||||
|
||||
if let title = title, !title.isEmpty {
|
||||
description += " '\(title)'"
|
||||
} else if let identifier = identifier, !identifier.isEmpty {
|
||||
description += " (\(identifier))"
|
||||
@MainActor public var domIdentifier: String? {
|
||||
get {
|
||||
var logs: [String] = [] // Logging for this specific getter, will be discarded if not used by caller
|
||||
return attribute(Attribute<String>(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: false, currentDebugLogs: &logs)
|
||||
}
|
||||
|
||||
return option == .verbose ? "<\(description)>" : description
|
||||
}
|
||||
|
||||
@MainActor public func domIdentifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
return attribute(Attribute<String>(AXAttributeNames.kAXDOMIdentifierAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,20 @@ import ApplicationServices // For AXUIElement and other C APIs
|
||||
import Foundation
|
||||
// We might need to import ValueHelpers or other local modules later
|
||||
|
||||
// MARK: - Environment Variable Check for JSON Logging
|
||||
// Copied from ElementSearch.swift - ideally this would be in a shared utility
|
||||
private func getEnvVar(_ name: String) -> String? {
|
||||
guard let value = getenv(name) else { return nil }
|
||||
return String(cString: value)
|
||||
}
|
||||
|
||||
private let AXORC_JSON_LOG_ENABLED: Bool = {
|
||||
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
|
||||
// Explicitly log the check to stderr for debugging the env var itself, specific to Element.swift
|
||||
fputs("[Element.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
|
||||
return envValue == "true"
|
||||
}()
|
||||
|
||||
// Element struct is NOT @MainActor. Isolation is applied to members that need it.
|
||||
public struct Element: Equatable, Hashable {
|
||||
public let underlyingElement: AXUIElement
|
||||
@ -26,7 +40,11 @@ public struct Element: Equatable, Hashable {
|
||||
@MainActor
|
||||
public func attribute<T>(_ attribute: Attribute<T>, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> T? {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line)) } }
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled && !AXORC_JSON_LOG_ENABLED {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
if T.self == [AXUIElement].self {
|
||||
dLog("Element.attribute: Special handling for T == [AXUIElement]. Attribute: \(attribute.rawValue)")
|
||||
@ -46,7 +64,7 @@ public struct Element: Equatable, Hashable {
|
||||
dLog("Element.attribute: Value for '\(attribute.rawValue)' was nil despite .success.")
|
||||
}
|
||||
} else if error == .noValue {
|
||||
dLog("Element.attribute: Attribute '\(attribute.rawValue)' has no value.")
|
||||
dLog("Element.attribute: Attribute '\(attribute.rawValue)' has no value.")
|
||||
} else {
|
||||
dLog("Element.attribute: Error fetching '\(attribute.rawValue)': \(error.rawValue)")
|
||||
}
|
||||
@ -77,24 +95,22 @@ public struct Element: Equatable, Hashable {
|
||||
}
|
||||
} else if T.self == Bool.self {
|
||||
if CFGetTypeID(unwrappedValue) == CFBooleanGetTypeID() {
|
||||
// Reverted to as! based on new compiler note
|
||||
let swiftBool = CFBooleanGetValue(unwrappedValue as! CFBoolean)
|
||||
let swiftBool = CFBooleanGetValue((unwrappedValue as! CFBoolean))
|
||||
return swiftBool as? T
|
||||
}
|
||||
} else if T.self == Int.self {
|
||||
if CFGetTypeID(unwrappedValue) == CFNumberGetTypeID() {
|
||||
if CFGetTypeID(unwrappedValue) == CFNumberGetTypeID() {
|
||||
var intValue: Int = 0
|
||||
// Reverted to as! based on new compiler note
|
||||
if CFNumberGetValue(unwrappedValue as! CFNumber, .sInt64Type, &intValue) {
|
||||
if CFNumberGetValue((unwrappedValue as! CFNumber), .sInt64Type, &intValue) {
|
||||
return intValue as? T
|
||||
}
|
||||
}
|
||||
} else if T.self == AXUIElement.self { // For single AXUIElement (e.g. parent)
|
||||
if CFGetTypeID(unwrappedValue) == AXUIElementGetTypeID() {
|
||||
if CFGetTypeID(unwrappedValue) == AXUIElementGetTypeID() {
|
||||
return unwrappedValue as? T // Direct cast should work as it's already AXUIElement
|
||||
}
|
||||
}
|
||||
} // Add other common types like NSNumber, AXValue (for CGPoint etc.) as needed
|
||||
|
||||
|
||||
// If no specific conversion worked, try a direct cast (might work for Any or some CF-bridged types)
|
||||
if let directCast = unwrappedValue as? T {
|
||||
dLog("Element.attribute: Basic conversion succeeded with direct cast for T = \(String(describing: T.self)), Attribute: \(attribute.rawValue).")
|
||||
@ -116,8 +132,8 @@ public struct Element: Equatable, Hashable {
|
||||
currentDebugLogs: inout [String]
|
||||
) -> CFTypeRef? {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
if isDebugLoggingEnabled && false {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
var value: CFTypeRef?
|
||||
@ -141,146 +157,12 @@ public struct Element: Equatable, Hashable {
|
||||
return nil // Return nil if not success or if value was nil (though success should mean value is populated)
|
||||
}
|
||||
|
||||
// MARK: - Common Attribute Getters (MOVED to Element+Properties.swift)
|
||||
// MARK: - Status Properties (MOVED to Element+Properties.swift)
|
||||
// MARK: - Hierarchy and Relationship Getters (Simpler ones MOVED to Element+Properties.swift)
|
||||
// MARK: - Action-related (supportedActions MOVED to Element+Properties.swift)
|
||||
|
||||
// Remaining properties and methods will stay here for now
|
||||
// (e.g., children, isActionSupported, performAction, parameterizedAttribute, briefDescription, generatePathString, static factories)
|
||||
// (e.g., children, parameterizedAttribute, briefDescription, generatePathString, static factories)
|
||||
// Action methods have been moved to Element+Actions.swift
|
||||
|
||||
// MOVED to Element+Hierarchy.swift
|
||||
// @MainActor public var children: [Element]? { ... }
|
||||
|
||||
// MARK: - Actions (supportedActions moved, other action methods remain)
|
||||
|
||||
@MainActor
|
||||
public func isActionSupported(_ actionName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> Bool {
|
||||
if let actions: [String] = attribute(
|
||||
Attribute<[String]>.actionNames,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
return actions.contains(actionName)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@discardableResult
|
||||
public func performAction(
|
||||
_ actionName: Attribute<String>,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) throws -> Element {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
let error = AXUIElementPerformAction(self.underlyingElement, actionName.rawValue as CFString)
|
||||
if error != .success {
|
||||
// Now call the refactored briefDescription, passing the logs along.
|
||||
let desc = self.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog("Action \(actionName.rawValue) failed on element \(desc). Error: \(error.rawValue)")
|
||||
throw AccessibilityError.actionFailed("Action \(actionName.rawValue) failed on element \(desc)", error)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@discardableResult
|
||||
public func performAction(_ actionName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) throws -> Element {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
let error = AXUIElementPerformAction(self.underlyingElement, actionName as CFString)
|
||||
if error != .success {
|
||||
// Now call the refactored briefDescription, passing the logs along.
|
||||
let desc = self.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)")
|
||||
throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
// MARK: - Parameterized Attributes
|
||||
|
||||
@MainActor
|
||||
public func parameterizedAttribute<T>(
|
||||
_ attribute: Attribute<T>,
|
||||
forParameter parameter: Any,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> T? {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var cfParameter: CFTypeRef?
|
||||
|
||||
// Convert Swift parameter to CFTypeRef for the API
|
||||
if var range = parameter as? CFRange {
|
||||
cfParameter = AXValueCreate(.cfRange, &range)
|
||||
} else if let string = parameter as? String {
|
||||
cfParameter = string as CFString
|
||||
} else if let number = parameter as? NSNumber {
|
||||
cfParameter = number
|
||||
} else if CFGetTypeID(parameter as CFTypeRef) != 0 { // Check if it's already a CFTypeRef-compatible type
|
||||
cfParameter = (parameter as CFTypeRef)
|
||||
} else {
|
||||
dLog("parameterizedAttribute: Unsupported parameter type \(type(of: parameter))")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let actualCFParameter = cfParameter else {
|
||||
dLog("parameterizedAttribute: Failed to convert parameter to CFTypeRef.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var value: CFTypeRef?
|
||||
let error = AXUIElementCopyParameterizedAttributeValue(
|
||||
underlyingElement,
|
||||
attribute.rawValue as CFString,
|
||||
actualCFParameter,
|
||||
&value
|
||||
)
|
||||
|
||||
if error != .success {
|
||||
dLog("parameterizedAttribute: Error \(error.rawValue) getting attribute \(attribute.rawValue)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let resultCFValue = value else { return nil }
|
||||
|
||||
// Use axValue's unwrapping and casting logic if possible, by temporarily creating an element and attribute
|
||||
// This is a bit of a conceptual stretch, as axValue is designed for direct attributes.
|
||||
// A more direct unwrap using ValueUnwrapper might be cleaner here.
|
||||
let unwrappedValue = ValueUnwrapper.unwrap(
|
||||
resultCFValue,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
guard let finalValue = unwrappedValue else { return nil }
|
||||
|
||||
// Perform type casting similar to axValue
|
||||
if T.self == String.self {
|
||||
if let str = finalValue as? String { return str as? T }
|
||||
else if let attrStr = finalValue as? NSAttributedString { return attrStr.string as? T }
|
||||
return nil
|
||||
}
|
||||
if let castedValue = finalValue as? T {
|
||||
return castedValue
|
||||
}
|
||||
dLog(
|
||||
"parameterizedAttribute: Fallback cast attempt for attribute '\(attribute.rawValue)' to type \(T.self) FAILED. Unwrapped value was \(type(of: finalValue)): \(finalValue)"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MOVED to Element+Hierarchy.swift
|
||||
// @MainActor
|
||||
// public func generatePathString() -> String { ... }
|
||||
|
||||
@ -299,28 +181,28 @@ public struct Element: Equatable, Hashable {
|
||||
public func computedName(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
// Now uses the passed-in logging parameters for its internal calls
|
||||
if let titleStr = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs),
|
||||
!titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr }
|
||||
!titleStr.isEmpty, titleStr != AXMiscConstants.kAXNotAvailableString { return titleStr }
|
||||
|
||||
if let valueStr: String = self.value(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) as? String, !valueStr.isEmpty, valueStr != kAXNotAvailableString { return valueStr }
|
||||
) as? String, !valueStr.isEmpty, valueStr != AXMiscConstants.kAXNotAvailableString { return valueStr }
|
||||
|
||||
if let descStr = self.description(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr }
|
||||
), !descStr.isEmpty, descStr != AXMiscConstants.kAXNotAvailableString { return descStr }
|
||||
|
||||
if let helpStr: String = self.attribute(
|
||||
Attribute<String>(kAXHelpAttribute),
|
||||
Attribute<String>(AXAttributeNames.kAXHelpAttribute),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr }
|
||||
), !helpStr.isEmpty, helpStr != AXMiscConstants.kAXNotAvailableString { return helpStr }
|
||||
if let phValueStr: String = self.attribute(
|
||||
Attribute<String>(kAXPlaceholderValueAttribute),
|
||||
Attribute<String>(AXAttributeNames.kAXPlaceholderValueAttribute),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
), !phValueStr.isEmpty, phValueStr != kAXNotAvailableString { return phValueStr }
|
||||
), !phValueStr.isEmpty, phValueStr != AXMiscConstants.kAXNotAvailableString { return phValueStr }
|
||||
|
||||
let roleNameStr: String = self.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
@ -330,7 +212,7 @@ public struct Element: Equatable, Hashable {
|
||||
if let roleDescStr: String = self.roleDescription(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
), !roleDescStr.isEmpty, roleDescStr != kAXNotAvailableString {
|
||||
), !roleDescStr.isEmpty, roleDescStr != AXMiscConstants.kAXNotAvailableString {
|
||||
return "\(roleDescStr) (\(roleNameStr))"
|
||||
}
|
||||
return nil
|
||||
@ -338,199 +220,3 @@ public struct Element: Equatable, Hashable {
|
||||
|
||||
// MARK: - Path and Hierarchy
|
||||
}
|
||||
|
||||
// Convenience factory for the application element - already @MainActor
|
||||
@MainActor
|
||||
public func applicationElement(for bundleIdOrName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> Element? {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
// Now call pid() with logging parameters
|
||||
guard let pid = pid(
|
||||
forAppIdentifier: bundleIdOrName,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
// dLog for "Failed to find PID..." is now handled inside pid() itself or if it returns nil here, we can log the higher level failure.
|
||||
// The message below is slightly redundant if pid() logs its own failure, but can be useful.
|
||||
dLog("applicationElement: Failed to obtain PID for '\(bundleIdOrName)'. Check previous logs from pid().")
|
||||
return nil
|
||||
}
|
||||
let appElement = AXUIElementCreateApplication(pid)
|
||||
return Element(appElement)
|
||||
}
|
||||
|
||||
// Convenience factory for the system-wide element - already @MainActor
|
||||
@MainActor
|
||||
public func systemWideElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element {
|
||||
// This function doesn't do much logging itself, but consistent signature is good.
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
dLog("Creating system-wide element.")
|
||||
return Element(AXUIElementCreateSystemWide())
|
||||
}
|
||||
|
||||
// Extension to generate a descriptive path string
|
||||
extension Element {
|
||||
@MainActor
|
||||
// Update signature to include logging parameters
|
||||
public func generatePathString(
|
||||
upTo ancestor: Element? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> String {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var pathComponents: [String] = []
|
||||
var currentElement: Element? = self
|
||||
|
||||
var depth = 0 // Safety break for very deep or circular hierarchies
|
||||
let maxDepth = 25
|
||||
var tempLogs: [String] = [] // Temporary logs for calls within the loop
|
||||
|
||||
dLog(
|
||||
"generatePathString started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "nil")"
|
||||
)
|
||||
|
||||
while let element = currentElement, depth < maxDepth {
|
||||
tempLogs.removeAll() // Clear for each iteration
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
pathComponents.append(briefDesc)
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Append logs from briefDescription
|
||||
|
||||
if let ancestor = ancestor, element == ancestor {
|
||||
dLog("generatePathString: Reached specified ancestor: \(briefDesc)")
|
||||
break // Reached the specified ancestor
|
||||
}
|
||||
|
||||
// Check role to prevent going above application or a window if its parent is the app
|
||||
tempLogs.removeAll()
|
||||
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentElement = element.parent(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentRole = parentElement?.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
if role == kAXApplicationRole ||
|
||||
(role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
|
||||
dLog(
|
||||
"generatePathString: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
currentElement = parentElement
|
||||
depth += 1
|
||||
if currentElement == nil && role != kAXApplicationRole {
|
||||
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
|
||||
dLog("generatePathString: Unexpected orphan: \(orphanLog)")
|
||||
pathComponents.append(orphanLog)
|
||||
break
|
||||
}
|
||||
}
|
||||
if depth >= maxDepth {
|
||||
dLog("generatePathString: Reached max depth (\(maxDepth)). Path might be truncated.")
|
||||
pathComponents.append("<...max_depth_reached...>")
|
||||
}
|
||||
|
||||
let finalPath = pathComponents.reversed().joined(separator: " -> ")
|
||||
dLog("generatePathString finished. Path: \(finalPath)")
|
||||
return finalPath
|
||||
}
|
||||
|
||||
// New function to return path components as an array
|
||||
@MainActor
|
||||
public func generatePathArray(
|
||||
upTo ancestor: Element? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> [String] {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var pathComponents: [String] = []
|
||||
var currentElement: Element? = self
|
||||
|
||||
var depth = 0
|
||||
let maxDepth = 25
|
||||
var tempLogs: [String] = []
|
||||
|
||||
dLog(
|
||||
"generatePathArray started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")"
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
while let element = currentElement, depth < maxDepth {
|
||||
tempLogs.removeAll()
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
pathComponents.append(briefDesc)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
if let ancestor = ancestor, element == ancestor {
|
||||
dLog("generatePathArray: Reached specified ancestor: \(briefDesc)")
|
||||
break
|
||||
}
|
||||
|
||||
tempLogs.removeAll()
|
||||
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentElement = element.parent(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
tempLogs.removeAll()
|
||||
let parentRole = parentElement?.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
|
||||
|
||||
if role == kAXApplicationRole ||
|
||||
(role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
|
||||
dLog(
|
||||
"generatePathArray: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
currentElement = parentElement
|
||||
depth += 1
|
||||
if currentElement == nil && role != kAXApplicationRole {
|
||||
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
|
||||
dLog("generatePathArray: Unexpected orphan: \(orphanLog)")
|
||||
pathComponents.append(orphanLog)
|
||||
break
|
||||
}
|
||||
}
|
||||
if depth >= maxDepth {
|
||||
dLog("generatePathArray: Reached max depth (\(maxDepth)). Path might be truncated.")
|
||||
pathComponents.append("<...max_depth_reached...>")
|
||||
}
|
||||
|
||||
let reversedPathComponents = Array(pathComponents.reversed())
|
||||
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
|
||||
return reversedPathComponents
|
||||
}
|
||||
}
|
||||
|
||||
37
Sources/AXorcist/Core/ElementFactories.swift
Normal file
37
Sources/AXorcist/Core/ElementFactories.swift
Normal file
@ -0,0 +1,37 @@
|
||||
// ElementFactories.swift - Factory functions for creating Element instances
|
||||
|
||||
import ApplicationServices // For AXUIElement and other C APIs
|
||||
import Foundation
|
||||
|
||||
// Convenience factory for the application element - already @MainActor
|
||||
@MainActor
|
||||
public func applicationElement(for bundleIdOrName: String, isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> Element? {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
// Now call pid() with logging parameters
|
||||
guard let pid = pid(
|
||||
forAppIdentifier: bundleIdOrName,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
// dLog for "Failed to find PID..." is now handled inside pid() itself or if it returns nil here, we can log the higher level failure.
|
||||
// The message below is slightly redundant if pid() logs its own failure, but can be useful.
|
||||
dLog("applicationElement: Failed to obtain PID for '\(bundleIdOrName)'. Check previous logs from pid().")
|
||||
return nil
|
||||
}
|
||||
let appElement = AXUIElementCreateApplication(pid)
|
||||
return Element(appElement)
|
||||
}
|
||||
|
||||
// Convenience factory for the system-wide element - already @MainActor
|
||||
@MainActor
|
||||
public func systemWideElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element {
|
||||
// This function doesn't do much logging itself, but consistent signature is good.
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
dLog("Creating system-wide element.")
|
||||
return Element(AXUIElementCreateSystemWide())
|
||||
}
|
||||
25
Sources/AXorcist/Core/ModelEnums.swift
Normal file
25
Sources/AXorcist/Core/ModelEnums.swift
Normal file
@ -0,0 +1,25 @@
|
||||
// ModelEnums.swift - Contains enum definitions for the AXorcist models
|
||||
|
||||
import Foundation
|
||||
|
||||
// Enum for output formatting options
|
||||
public enum OutputFormat: String, Codable {
|
||||
case smart // Default, tries to be concise and informative
|
||||
case verbose // More detailed output, includes more attributes/info
|
||||
case text_content // Primarily extracts textual content
|
||||
case json_string // Returns the attributes as a JSON string (new)
|
||||
}
|
||||
|
||||
// Define CommandType enum
|
||||
public enum CommandType: String, Codable {
|
||||
case query
|
||||
case performAction = "performAction"
|
||||
case getAttributes = "getAttributes"
|
||||
case batch
|
||||
case describeElement = "describeElement"
|
||||
case getFocusedElement = "getFocusedElement"
|
||||
case collectAll = "collectAll"
|
||||
case extractText = "extractText"
|
||||
case ping
|
||||
// Add future commands here, ensuring case matches JSON or provide explicit raw value
|
||||
}
|
||||
@ -1,376 +0,0 @@
|
||||
// Models.swift - Contains Codable structs for command handling and responses
|
||||
|
||||
import Foundation
|
||||
|
||||
// Enum for output formatting options
|
||||
public enum OutputFormat: String, Codable {
|
||||
case smart // Default, tries to be concise and informative
|
||||
case verbose // More detailed output, includes more attributes/info
|
||||
case text_content // Primarily extracts textual content
|
||||
case json_string // Returns the attributes as a JSON string (new)
|
||||
}
|
||||
|
||||
// Define CommandType enum
|
||||
public enum CommandType: String, Codable {
|
||||
case query
|
||||
case performAction = "performAction"
|
||||
case getAttributes = "getAttributes"
|
||||
case batch
|
||||
case describeElement = "describeElement"
|
||||
case getFocusedElement = "getFocusedElement"
|
||||
case collectAll = "collectAll"
|
||||
case extractText = "extractText"
|
||||
case ping
|
||||
// Add future commands here, ensuring case matches JSON or provide explicit raw value
|
||||
}
|
||||
|
||||
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
|
||||
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 int32 = try? container.decode(Int32.self) {
|
||||
self.value = int32
|
||||
} else if let int64 = try? container.decode(Int64.self) {
|
||||
self.value = int64
|
||||
} else if let uint = try? container.decode(UInt.self) {
|
||||
self.value = uint
|
||||
} else if let uint32 = try? container.decode(UInt32.self) {
|
||||
self.value = uint32
|
||||
} else if let uint64 = try? container.decode(UInt64.self) {
|
||||
self.value = uint64
|
||||
} else if let double = try? container.decode(Double.self) {
|
||||
self.value = double
|
||||
} else if let float = try? container.decode(Float.self) {
|
||||
self.value = float
|
||||
} 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()
|
||||
switch value {
|
||||
case is Void:
|
||||
try container.encodeNil()
|
||||
case let bool as Bool:
|
||||
try container.encode(bool)
|
||||
case let int as Int:
|
||||
try container.encode(int)
|
||||
case let int32 as Int32:
|
||||
try container.encode(Int(int32))
|
||||
case let int64 as Int64:
|
||||
try container.encode(int64)
|
||||
case let uint as UInt:
|
||||
try container.encode(uint)
|
||||
case let uint32 as UInt32:
|
||||
try container.encode(uint32)
|
||||
case let uint64 as UInt64:
|
||||
try container.encode(uint64)
|
||||
case let double as Double:
|
||||
try container.encode(double)
|
||||
case let float as Float:
|
||||
try container.encode(float)
|
||||
case let string as String:
|
||||
try container.encode(string)
|
||||
case let array as [AnyCodable]:
|
||||
try container.encode(array)
|
||||
case let array as [Any?]:
|
||||
try container.encode(array.map { AnyCodable($0) })
|
||||
case let dictionary as [String: AnyCodable]:
|
||||
try container.encode(dictionary)
|
||||
case let dictionary as [String: Any?]:
|
||||
try container.encode(dictionary.mapValues { AnyCodable($0) })
|
||||
default:
|
||||
let context = EncodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "AnyCodable value cannot be encoded"
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type alias for element attributes dictionary
|
||||
public typealias ElementAttributes = [String: AnyCodable]
|
||||
|
||||
// Main command envelope - REPLACED with definition from axorc.swift for consistency
|
||||
public struct CommandEnvelope: Codable {
|
||||
public let command_id: 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 debug_logging: Bool?
|
||||
public let locator: Locator? // Locator from this file
|
||||
public let path_hint: [String]?
|
||||
public let max_elements: Int?
|
||||
public let output_format: OutputFormat? // OutputFormat from this file
|
||||
public let action_name: String? // For performAction
|
||||
public let action_value: AnyCodable? // For performAction (AnyCodable from this file)
|
||||
public let sub_commands: [CommandEnvelope]? // For batch command
|
||||
|
||||
// Added a public initializer for convenience, matching fields.
|
||||
public init(command_id: String,
|
||||
command: CommandType,
|
||||
application: String? = nil,
|
||||
attributes: [String]? = nil,
|
||||
payload: [String: String]? = nil,
|
||||
debug_logging: Bool? = nil,
|
||||
locator: Locator? = nil,
|
||||
path_hint: [String]? = nil,
|
||||
max_elements: Int? = nil,
|
||||
output_format: OutputFormat? = nil,
|
||||
action_name: String? = nil,
|
||||
action_value: AnyCodable? = nil,
|
||||
sub_commands: [CommandEnvelope]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.command = command
|
||||
self.application = application
|
||||
self.attributes = attributes
|
||||
self.payload = payload
|
||||
self.debug_logging = debug_logging
|
||||
self.locator = locator
|
||||
self.path_hint = path_hint
|
||||
self.max_elements = max_elements
|
||||
self.output_format = output_format
|
||||
self.action_name = action_name
|
||||
self.action_value = action_value
|
||||
self.sub_commands = sub_commands
|
||||
}
|
||||
}
|
||||
|
||||
// Locator for finding elements
|
||||
public struct Locator: Codable, Sendable {
|
||||
public var match_all: Bool?
|
||||
public var criteria: [String: String]
|
||||
public var root_element_path_hint: [String]?
|
||||
public var requireAction: String?
|
||||
public var computed_name_contains: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case match_all
|
||||
case criteria
|
||||
case root_element_path_hint
|
||||
case requireAction = "require_action"
|
||||
case computed_name_contains
|
||||
}
|
||||
|
||||
public init(match_all: Bool? = nil, criteria: [String: String] = [:], root_element_path_hint: [String]? = nil,
|
||||
requireAction: String? = nil, computed_name_contains: String? = nil) {
|
||||
self.match_all = match_all
|
||||
self.criteria = criteria
|
||||
self.root_element_path_hint = root_element_path_hint
|
||||
self.requireAction = requireAction
|
||||
self.computed_name_contains = computed_name_contains
|
||||
}
|
||||
}
|
||||
|
||||
// Response for query command (single element)
|
||||
public struct QueryResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var command: String
|
||||
public var data: AXElement?
|
||||
public var attributes: ElementAttributes?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool = true, command: String = "getFocusedElement", data: AXElement? = nil,
|
||||
attributes: ElementAttributes? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.command = command
|
||||
self.data = data
|
||||
self.attributes = attributes
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
|
||||
// Custom init for HandlerResponse integration
|
||||
public init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.command = command
|
||||
self.data = handlerResponse.data
|
||||
// If HandlerResponse has attributes, map them from its data field.
|
||||
self.attributes = handlerResponse.data?.attributes
|
||||
self.error = handlerResponse.error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Response for collect_all command (multiple elements)
|
||||
public struct MultiQueryResponse: Codable {
|
||||
public var command_id: String
|
||||
public var elements: [ElementAttributes]?
|
||||
public var count: Int?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, elements: [ElementAttributes]? = nil, count: Int? = nil, error: String? = nil,
|
||||
debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.elements = elements
|
||||
self.count = count ?? elements?.count
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Response for perform_action command
|
||||
public struct PerformResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Response for extract_text command
|
||||
public struct TextContentResponse: Codable {
|
||||
public var command_id: String
|
||||
public var text_content: String?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, text_content: String? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.text_content = text_content
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Generic error response
|
||||
public struct ErrorResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var error: ErrorDetail
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, error: String, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = false
|
||||
self.error = ErrorDetail(message: error)
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
public struct ErrorDetail: Codable {
|
||||
public var message: String
|
||||
|
||||
public init(message: String) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
// Simple success response, e.g. for ping
|
||||
public struct SimpleSuccessResponse: Codable, Equatable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var status: String
|
||||
public var message: String
|
||||
public var details: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, status: String, message: String, details: String? = nil,
|
||||
debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = true
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.details = details
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder for any additional models if needed
|
||||
|
||||
public struct AXElement: Codable, Sendable {
|
||||
public var attributes: ElementAttributes?
|
||||
public var path: [String]?
|
||||
|
||||
public init(attributes: ElementAttributes?, path: [String]? = nil) {
|
||||
self.attributes = attributes
|
||||
self.path = path
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to add JSON encoding functionality to QueryResponse
|
||||
extension QueryResponse {
|
||||
public func jsonString() throws -> String {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(self)
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handler Response Models
|
||||
|
||||
public struct HandlerResponse: Codable, Sendable {
|
||||
public var data: AXElement?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(data: AXElement? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.data = data
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Structure for custom JSON output of handleCollectAll
|
||||
internal struct CollectAllOutput: Encodable {
|
||||
let command_id: String
|
||||
let success: Bool
|
||||
let command: String
|
||||
let collected_elements: [AXElement]
|
||||
let app_bundle_id: String?
|
||||
let debug_logs: [String]?
|
||||
}
|
||||
|
||||
// ADDED BatchResponse struct
|
||||
public struct BatchResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var results: [HandlerResponse] // Array of HandlerResponses for each sub-command
|
||||
public var error: String? // For an overall batch error, if any
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool, results: [HandlerResponse], error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.results = results
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
@ -12,4 +12,4 @@ public enum PathUtils {
|
||||
}
|
||||
return (attributeName: String(parts[0]), expectedValue: String(parts[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
Sources/AXorcist/Core/ResponseModels.swift
Normal file
176
Sources/AXorcist/Core/ResponseModels.swift
Normal file
@ -0,0 +1,176 @@
|
||||
// ResponseModels.swift - Contains response model structs for AXorcist commands
|
||||
|
||||
import Foundation
|
||||
|
||||
// Response for query command (single element)
|
||||
public struct QueryResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var command: String
|
||||
public var data: AXElement?
|
||||
public var attributes: ElementAttributes?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool = true, command: String = "getFocusedElement", data: AXElement? = nil,
|
||||
attributes: ElementAttributes? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.command = command
|
||||
self.data = data
|
||||
self.attributes = attributes
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
|
||||
// Custom init for HandlerResponse integration
|
||||
public init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.command = command
|
||||
self.data = handlerResponse.data
|
||||
// If HandlerResponse has attributes, map them from its data field.
|
||||
self.attributes = handlerResponse.data?.attributes
|
||||
self.error = handlerResponse.error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to add JSON encoding functionality to QueryResponse
|
||||
extension QueryResponse {
|
||||
public func jsonString() throws -> String {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(self)
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// Response for collect_all command (multiple elements)
|
||||
public struct MultiQueryResponse: Codable {
|
||||
public var command_id: String
|
||||
public var elements: [ElementAttributes]?
|
||||
public var count: Int?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, elements: [ElementAttributes]? = nil, count: Int? = nil, error: String? = nil,
|
||||
debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.elements = elements
|
||||
self.count = count ?? elements?.count
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Response for perform_action command
|
||||
public struct PerformResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Response for extract_text command
|
||||
public struct TextContentResponse: Codable {
|
||||
public var command_id: String
|
||||
public var text_content: String?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, text_content: String? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.text_content = text_content
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Generic error response
|
||||
public struct ErrorResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var error: ErrorDetail
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, error: String, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = false
|
||||
self.error = ErrorDetail(message: error)
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
public struct ErrorDetail: Codable {
|
||||
public var message: String
|
||||
|
||||
public init(message: String) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
// Simple success response, e.g. for ping
|
||||
public struct SimpleSuccessResponse: Codable, Equatable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var status: String
|
||||
public var message: String
|
||||
public var details: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, status: String, message: String, details: String? = nil,
|
||||
debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = true
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.details = details
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
public struct HandlerResponse: Codable {
|
||||
public var data: AXElement?
|
||||
public var error: String?
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(data: AXElement? = nil, error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.data = data
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
public struct BatchResponse: Codable {
|
||||
public var command_id: String
|
||||
public var success: Bool
|
||||
public var results: [HandlerResponse] // Array of HandlerResponses for each sub-command
|
||||
public var error: String? // For an overall batch error, if any
|
||||
public var debug_logs: [String]?
|
||||
|
||||
public init(command_id: String, success: Bool, results: [HandlerResponse], error: String? = nil, debug_logs: [String]? = nil) {
|
||||
self.command_id = command_id
|
||||
self.success = success
|
||||
self.results = results
|
||||
self.error = error
|
||||
self.debug_logs = debug_logs
|
||||
}
|
||||
}
|
||||
|
||||
// Structure for custom JSON output of handleCollectAll
|
||||
internal struct CollectAllOutput: Encodable {
|
||||
let command_id: String
|
||||
let success: Bool
|
||||
let command: String
|
||||
let collected_elements: [AXElement]
|
||||
let app_bundle_id: String?
|
||||
let debug_logs: [String]?
|
||||
}
|
||||
@ -5,9 +5,103 @@ import ApplicationServices
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
// MARK: - Environment Variable Check for JSON Logging
|
||||
// (Copied from other files - consider a shared utility)
|
||||
private func getEnvVar(_ name: String) -> String? {
|
||||
guard let value = getenv(name) else { return nil }
|
||||
return String(cString: value)
|
||||
}
|
||||
|
||||
private let HANDLER_AXORC_JSON_LOG_ENABLED: Bool = {
|
||||
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
|
||||
// No fputs here, assuming it's primarily for Swift module debugging
|
||||
return envValue == "true"
|
||||
}()
|
||||
|
||||
// MARK: - Action & Data Handlers Extension
|
||||
extension AXorcist {
|
||||
|
||||
// MARK: - Private Helper Methods
|
||||
|
||||
private func executeStandardAccessibilityAction(
|
||||
_ axActionName: CFString,
|
||||
on targetElement: Element,
|
||||
actionNameForLog: String,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> AXError {
|
||||
let axStatus = AXUIElementPerformAction(targetElement.underlyingElement, axActionName)
|
||||
if axStatus != .success {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Failed to perform \(actionNameForLog) action: \(axErrorToString(axStatus))"
|
||||
currentDebugLogs.append(errorMessage)
|
||||
}
|
||||
return axStatus
|
||||
}
|
||||
|
||||
private func executeSetAttributeValueAction(
|
||||
attributeName: String,
|
||||
value: AnyCodable?,
|
||||
on targetElement: Element,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> (errorMessage: String?, axStatus: AXError) {
|
||||
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
|
||||
if attributeName.hasPrefix("AX") {
|
||||
let axStatus = AXUIElementPerformAction(targetElement.underlyingElement, attributeName as CFString)
|
||||
if axStatus != .success {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Failed to perform action '\(attributeName)': \(axErrorToString(axStatus))"
|
||||
return (errorMessage, axStatus)
|
||||
}
|
||||
return (nil, axStatus)
|
||||
} else {
|
||||
guard let actionValue = value else {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Attribute action '\(attributeName)' requires an action_value, but none was provided."
|
||||
return (errorMessage, .invalidUIElement)
|
||||
}
|
||||
|
||||
var cfValue: CFTypeRef?
|
||||
switch actionValue.value {
|
||||
case let stringValue as String:
|
||||
cfValue = stringValue as CFString
|
||||
case let boolValue as Bool:
|
||||
cfValue = boolValue as CFBoolean
|
||||
case let intValue as Int:
|
||||
var number = intValue
|
||||
cfValue = CFNumberCreate(kCFAllocatorDefault, .intType, &number)
|
||||
case let doubleValue as Double:
|
||||
var number = doubleValue
|
||||
cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number)
|
||||
default:
|
||||
if CFGetTypeID(actionValue.value as AnyObject) != 0 {
|
||||
cfValue = actionValue.value as AnyObject
|
||||
dLog("[AXorcist.handlePerformAction] Warning: Attempting to use actionValue of type '\(type(of: actionValue.value))' directly as CFTypeRef for attribute '\(attributeName)'. This might not work as expected.")
|
||||
} else {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Unsupported value type '\(type(of: actionValue.value))' for attribute '\(attributeName)'. Cannot convert to CFTypeRef."
|
||||
dLog(errorMessage)
|
||||
return (errorMessage, .invalidUIElement)
|
||||
}
|
||||
}
|
||||
|
||||
guard let finalCFValue = cfValue else {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(attributeName)' to a CoreFoundation type."
|
||||
return (errorMessage, .invalidUIElement)
|
||||
}
|
||||
|
||||
let axStatus = AXUIElementSetAttributeValue(targetElement.underlyingElement, attributeName as CFString, finalCFValue)
|
||||
if axStatus != .success {
|
||||
let errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(attributeName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
|
||||
return (errorMessage, axStatus)
|
||||
}
|
||||
|
||||
return (nil, axStatus)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func handlePerformAction(
|
||||
for appIdentifierOrNil: String? = nil,
|
||||
@ -18,7 +112,7 @@ extension AXorcist {
|
||||
maxDepth: Int? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> HandlerResponse {
|
||||
) async -> HandlerResponse {
|
||||
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
@ -29,74 +123,26 @@ extension AXorcist {
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[AXorcist.handlePerformAction] Handling for app: \(appIdentifier), action: \(actionName)")
|
||||
|
||||
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) else {
|
||||
let error = "[AXorcist.handlePerformAction] Failed to get application element for identifier: \(appIdentifier)"
|
||||
currentDebugLogs.append(error)
|
||||
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
|
||||
let targetElementResult = await self.findTargetElement(
|
||||
for: appIdentifierOrNil,
|
||||
locator: locator,
|
||||
pathHint: pathHint,
|
||||
isRootedAtApp: true,
|
||||
baseElement: nil,
|
||||
maxDepthForSearch: maxDepth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
let targetElement: Element
|
||||
switch targetElementResult {
|
||||
case .success(let element):
|
||||
targetElement = element
|
||||
case .failure(let error):
|
||||
return HandlerResponse(data: nil, error: error.message, debug_logs: error.logs ?? currentDebugLogs)
|
||||
}
|
||||
|
||||
var effectiveElement = appElement
|
||||
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("[AXorcist.handlePerformAction] Navigating with path_hint: \(pathHint.joined(separator: " -> ")) from root \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) else {
|
||||
let lastLogBeforeDebug = currentDebugLogs.last
|
||||
let error: String
|
||||
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
|
||||
error = "Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
|
||||
error = "Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else {
|
||||
error = "[AXorcist.handlePerformAction] Failed to navigate using path hint: \(pathHint.joined(separator: " -> "))"
|
||||
}
|
||||
|
||||
if isDebugLoggingEnabled {
|
||||
if let actualLastLog = lastLogBeforeDebug {
|
||||
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(error)'. LastLog: '\(actualLastLog)'")
|
||||
} else {
|
||||
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(error)'")
|
||||
}
|
||||
}
|
||||
currentDebugLogs.append(error)
|
||||
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
|
||||
}
|
||||
effectiveElement = navigatedElement
|
||||
dLog("[AXorcist.handlePerformAction] Successfully navigated path_hint. New effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
}
|
||||
|
||||
let targetElementForAction: Element
|
||||
if let actualLocator = locator {
|
||||
dLog("[AXorcist.handlePerformAction] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) using locator criteria: \(actualLocator.criteria)")
|
||||
|
||||
let searchResult = search(
|
||||
element: effectiveElement,
|
||||
locator: actualLocator,
|
||||
requireAction: actualLocator.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
fputs("HANDLER_RAW_STDERR_BEFORE_LOG_APPEND handlePerformAction: searchResult.logs.count = \(searchResult.logs.count), currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
|
||||
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for performAction")
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
fputs("HANDLER_RAW_STDERR_AFTER_LOG_APPEND handlePerformAction: currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
|
||||
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_HANDLER")
|
||||
|
||||
guard let foundElement = searchResult.foundElement else {
|
||||
let error = "[AXorcist.handlePerformAction] Search failed. Could not find element matching locator criteria \(actualLocator.criteria) starting from element \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))."
|
||||
if !currentDebugLogs.contains(error) {
|
||||
currentDebugLogs.append(error)
|
||||
}
|
||||
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
|
||||
}
|
||||
targetElementForAction = foundElement
|
||||
dLog("[AXorcist.handlePerformAction] Found element via locator: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
} else {
|
||||
targetElementForAction = effectiveElement
|
||||
dLog("[AXorcist.handlePerformAction] No locator provided. Using current effectiveElement as target: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
}
|
||||
|
||||
dLog("[AXorcist.handlePerformAction] Element for action: \(targetElementForAction.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
dLog("[AXorcist.handlePerformAction] Element for action: \(targetElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
if let actionValue = actionValue {
|
||||
let valueDescription = String(describing: actionValue.value)
|
||||
dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)' with value: \(valueDescription)")
|
||||
@ -109,77 +155,75 @@ extension AXorcist {
|
||||
|
||||
switch actionName.lowercased() {
|
||||
case "press":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPressAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXPressAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "press",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform press action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
case "increment":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXIncrementAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXIncrementAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "increment",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform increment action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
case "decrement":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXDecrementAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXDecrementAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "decrement",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform decrement action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
case "showmenu":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXShowMenuAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXShowMenuAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "showmenu",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform showmenu action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
case "pick":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXPickAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXPickAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "pick",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform pick action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
case "cancel":
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, kAXCancelAction as CFString)
|
||||
axStatus = self.executeStandardAccessibilityAction(
|
||||
AXActionNames.kAXCancelAction as CFString,
|
||||
on: targetElement,
|
||||
actionNameForLog: "cancel",
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform cancel action: \(axErrorToString(axStatus))"
|
||||
}
|
||||
default:
|
||||
if actionName.hasPrefix("AX") {
|
||||
axStatus = AXUIElementPerformAction(targetElementForAction.underlyingElement, actionName as CFString)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to perform action '\(actionName)': \(axErrorToString(axStatus))"
|
||||
}
|
||||
} else {
|
||||
if let actionValue = actionValue {
|
||||
var cfValue: CFTypeRef?
|
||||
switch actionValue.value {
|
||||
case let stringValue as String:
|
||||
cfValue = stringValue as CFString
|
||||
case let boolValue as Bool:
|
||||
cfValue = boolValue as CFBoolean
|
||||
case let intValue as Int:
|
||||
var number = intValue
|
||||
cfValue = CFNumberCreate(kCFAllocatorDefault, .intType, &number)
|
||||
case let doubleValue as Double:
|
||||
var number = doubleValue
|
||||
cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number)
|
||||
default:
|
||||
if CFGetTypeID(actionValue.value as AnyObject) != 0 {
|
||||
cfValue = actionValue.value as AnyObject
|
||||
dLog("[AXorcist.handlePerformAction] Warning: Attempting to use actionValue of type '\(type(of: actionValue.value))' directly as CFTypeRef for attribute '\(actionName)'. This might not work as expected.")
|
||||
} else {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Unsupported value type '\(type(of: actionValue.value))' for attribute '\(actionName)'. Cannot convert to CFTypeRef."
|
||||
dLog(errorMessage!)
|
||||
}
|
||||
}
|
||||
|
||||
if errorMessage == nil, let finalCFValue = cfValue {
|
||||
axStatus = AXUIElementSetAttributeValue(targetElementForAction.underlyingElement, actionName as CFString, finalCFValue)
|
||||
if axStatus != .success {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(actionName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
|
||||
}
|
||||
} else if errorMessage == nil {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type."
|
||||
}
|
||||
} else {
|
||||
errorMessage = "[AXorcist.handlePerformAction] Attribute action '\(actionName)' requires an action_value, but none was provided."
|
||||
}
|
||||
}
|
||||
let result = self.executeSetAttributeValueAction(
|
||||
attributeName: actionName,
|
||||
value: actionValue,
|
||||
on: targetElement,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
errorMessage = result.errorMessage
|
||||
axStatus = result.axStatus
|
||||
}
|
||||
|
||||
if let currentErrorMessage = errorMessage {
|
||||
@ -198,7 +242,7 @@ extension AXorcist {
|
||||
pathHint: [String]? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> HandlerResponse {
|
||||
) async -> HandlerResponse {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
@ -208,126 +252,81 @@ extension AXorcist {
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[handleExtractText] Starting text extraction for app: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
let targetElementResult = await self.findTargetElement(
|
||||
for: appIdentifierOrNil,
|
||||
locator: locator,
|
||||
pathHint: pathHint,
|
||||
isRootedAtApp: true,
|
||||
baseElement: nil,
|
||||
maxDepthForSearch: nil,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "[handleExtractText] Failed to get application element for \(appIdentifier)"
|
||||
currentDebugLogs.append(errorMessage)
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
)
|
||||
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("[handleExtractText] Navigating to element using path hint: \(pathHint.joined(separator: " -> "))")
|
||||
guard let navigatedElement = navigateToElement(
|
||||
from: appElement,
|
||||
pathHint: pathHint,
|
||||
let targetElementForExtract: Element
|
||||
let appElement: Element
|
||||
switch targetElementResult {
|
||||
case .success(let element):
|
||||
targetElementForExtract = element
|
||||
// We need the app element for path generation, so get it separately
|
||||
guard let appEl = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let lastLogBeforeDebug = currentDebugLogs.last
|
||||
let errorMessage: String
|
||||
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
|
||||
errorMessage = "[handleExtractText] Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
|
||||
errorMessage = "[handleExtractText] Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else {
|
||||
errorMessage = "[handleExtractText] Failed to navigate to element using path hint: \(pathHint.joined(separator: " -> "))"
|
||||
}
|
||||
|
||||
if isDebugLoggingEnabled {
|
||||
if let actualLastLog = lastLogBeforeDebug {
|
||||
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(errorMessage)'. LastLog: '\(actualLastLog)'")
|
||||
} else {
|
||||
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(errorMessage)'")
|
||||
}
|
||||
}
|
||||
let errorMessage = "[handleExtractText] Failed to get application element for path generation: \(appIdentifier)"
|
||||
currentDebugLogs.append(errorMessage)
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
effectiveElement = navigatedElement
|
||||
dLog("[handleExtractText] Successfully navigated path_hint. New effectiveElement for text extraction: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
appElement = appEl
|
||||
case .failure(let error):
|
||||
return HandlerResponse(data: nil, error: error.message, debug_logs: error.logs ?? currentDebugLogs)
|
||||
}
|
||||
|
||||
let targetElementForExtract: Element
|
||||
if let actualLocator = locator {
|
||||
dLog("[handleExtractText] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) using locator criteria: \(actualLocator.criteria)")
|
||||
|
||||
let searchResult = search(
|
||||
element: effectiveElement,
|
||||
locator: actualLocator,
|
||||
requireAction: nil,
|
||||
depth: 0,
|
||||
maxDepth: DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
fputs("HANDLER_RAW_STDERR_BEFORE_LOG_APPEND handleExtractText: searchResult.logs.count = \(searchResult.logs.count), currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
|
||||
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for extractText")
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
fputs("HANDLER_RAW_STDERR_AFTER_LOG_APPEND handleExtractText: currentDebugLogs count = \(currentDebugLogs.count)\n", stderr)
|
||||
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_EXTRACT_TEXT")
|
||||
|
||||
guard let foundElement = searchResult.foundElement else {
|
||||
let errorMessage = "[handleExtractText] Target element not found for locator: \(actualLocator) starting from \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))"
|
||||
if !currentDebugLogs.contains(errorMessage) {
|
||||
currentDebugLogs.append(errorMessage)
|
||||
}
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
targetElementForExtract = foundElement
|
||||
dLog("[handleExtractText] Found element via locator for text extraction: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
} else {
|
||||
targetElementForExtract = effectiveElement
|
||||
dLog("[handleExtractText] No locator. Using effectiveElement from path_hint/app_root as target for text extraction: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
}
|
||||
|
||||
dLog("[handleExtractText] Target element found: \(targetElementForExtract.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)), attempting to extract text")
|
||||
var attributes: [String: AnyCodable] = [:]
|
||||
var extractedAnyText = false
|
||||
|
||||
if let valueCF = targetElementForExtract.rawAttributeValue(named: kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
if let valueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXValueAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
if CFGetTypeID(valueCF) == CFStringGetTypeID() {
|
||||
let extractedValueText = valueCF as! String
|
||||
if !extractedValueText.isEmpty {
|
||||
attributes["extractedValue"] = AnyCodable(extractedValueText)
|
||||
extractedAnyText = true
|
||||
dLog("[handleExtractText] Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
|
||||
dLog("[handleExtractText] Extracted text from AXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXValueAttribute was empty or not a string.")
|
||||
dLog("[handleExtractText] AXValueAttribute was empty or not a string.")
|
||||
}
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(valueCF))")
|
||||
dLog("[handleExtractText] AXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(valueCF))")
|
||||
}
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXValueAttribute not found or nil.")
|
||||
dLog("[handleExtractText] AXValueAttribute not found or nil.")
|
||||
}
|
||||
|
||||
if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: kAXSelectedTextAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
if let selectedValueCF = targetElementForExtract.rawAttributeValue(named: AXAttributeNames.kAXSelectedTextAttribute as String, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
if CFGetTypeID(selectedValueCF) == CFStringGetTypeID() {
|
||||
let extractedSelectedText = selectedValueCF as! String
|
||||
if !extractedSelectedText.isEmpty {
|
||||
attributes["extractedSelectedText"] = AnyCodable(extractedSelectedText)
|
||||
extractedAnyText = true
|
||||
dLog("[handleExtractText] Extracted selected text from kAXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
|
||||
dLog("[handleExtractText] Extracted selected text from AXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXSelectedTextAttribute was empty or not a string.")
|
||||
dLog("[handleExtractText] AXSelectedTextAttribute was empty or not a string.")
|
||||
}
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValueCF))")
|
||||
dLog("[handleExtractText] AXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValueCF))")
|
||||
}
|
||||
} else {
|
||||
dLog("[handleExtractText] kAXSelectedTextAttribute not found or nil.")
|
||||
dLog("[handleExtractText] AXSelectedTextAttribute not found or nil.")
|
||||
}
|
||||
|
||||
|
||||
if !extractedAnyText {
|
||||
dLog("[handleExtractText] No text could be extracted from kAXValue or kAXSelectedText for element.")
|
||||
dLog("[handleExtractText] No text could be extracted from AXValue or AXSelectedText for element.")
|
||||
}
|
||||
|
||||
let pathArray = targetElementForExtract.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
let axElementToReturn = AXElement(attributes: attributes, path: pathArray)
|
||||
let axElementToReturn = AXElement(attributes: attributes, path: pathArray)
|
||||
return HandlerResponse(data: axElementToReturn, error: nil, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ extension AXorcist {
|
||||
subCommands: [CommandEnvelope], // The array of sub-commands to process
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> [HandlerResponse] {
|
||||
) async -> [HandlerResponse] {
|
||||
// Local debug logging function
|
||||
func dLog(_ message: String, subCommandID: String? = nil) {
|
||||
if isDebugLoggingEnabled {
|
||||
@ -56,7 +56,7 @@ extension AXorcist {
|
||||
) // Keep debug_logs nil for specific error, main logs will have the dLog entry
|
||||
break
|
||||
}
|
||||
subCommandResponse = self.handleGetAttributes(
|
||||
subCommandResponse = await self.handleGetAttributes(
|
||||
for: subCommandEnvelope.application,
|
||||
locator: locator,
|
||||
requestedAttributes: subCommandEnvelope.attributes,
|
||||
@ -74,7 +74,7 @@ extension AXorcist {
|
||||
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
|
||||
break
|
||||
}
|
||||
subCommandResponse = self.handleQuery(
|
||||
subCommandResponse = await self.handleQuery(
|
||||
for: subCommandEnvelope.application,
|
||||
locator: locator,
|
||||
pathHint: subCommandEnvelope.path_hint,
|
||||
@ -92,11 +92,12 @@ extension AXorcist {
|
||||
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
|
||||
break
|
||||
}
|
||||
subCommandResponse = self.handleDescribeElement(
|
||||
subCommandResponse = await self.handleDescribeElement(
|
||||
for: subCommandEnvelope.application,
|
||||
locator: locator,
|
||||
pathHint: subCommandEnvelope.path_hint,
|
||||
maxDepth: subCommandEnvelope.max_elements,
|
||||
requestedAttributes: subCommandEnvelope.attributes,
|
||||
outputFormat: subCommandEnvelope.output_format,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
@ -113,7 +114,7 @@ extension AXorcist {
|
||||
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
guard let actionName = subCommandEnvelope.action_name else {
|
||||
let errorMsg = "Action name missing for performAction in batch (sub-command ID: \(subCmdID))"
|
||||
dLog(errorMsg, subCommandID: subCmdID)
|
||||
@ -124,7 +125,7 @@ extension AXorcist {
|
||||
// If only path_hint is provided, locator will be nil, which is fine for handlePerformAction.
|
||||
// If only locator is provided, pathHint will be nil or empty, also fine.
|
||||
// If both, handlePerformAction can use pathHint as root_element_path_hint for the locator.
|
||||
subCommandResponse = self.handlePerformAction(
|
||||
subCommandResponse = await self.handlePerformAction(
|
||||
for: subCommandEnvelope.application,
|
||||
locator: subCommandEnvelope.locator, // Pass along, might be nil
|
||||
pathHint: subCommandEnvelope.path_hint, // Pass along, might be nil or empty
|
||||
@ -142,7 +143,7 @@ extension AXorcist {
|
||||
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
|
||||
break
|
||||
}
|
||||
subCommandResponse = self.handleExtractText(
|
||||
subCommandResponse = await self.handleExtractText(
|
||||
for: subCommandEnvelope.application,
|
||||
locator: locator,
|
||||
pathHint: subCommandEnvelope.path_hint,
|
||||
|
||||
@ -62,7 +62,7 @@ extension AXorcist {
|
||||
"[AXorcist.handleCollectAll] Starting. App: \(appNameForLog), Locator: \(locatorDesc), PathHint: \(pathHintDesc), MaxDepth: \(maxDepthDesc)"
|
||||
)
|
||||
|
||||
let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXorcist.defaultMaxDepthCollectAll
|
||||
let recursionDepthLimit = (maxDepth != nil && maxDepth! >= 0) ? maxDepth! : AXMiscConstants.defaultMaxDepthCollectAll
|
||||
let attributesToFetch = requestedAttributes ?? AXorcist.defaultAttributesToFetch
|
||||
let effectiveOutputFormat = outputFormat ?? .smart
|
||||
|
||||
@ -125,16 +125,15 @@ extension AXorcist {
|
||||
|
||||
if let loc = locator {
|
||||
dLog("Locator provided. Searching for element from current startElement: \(startElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) with locator criteria: \(String(describing: loc.criteria))")
|
||||
|
||||
let searchResultCollectAll = search(element: startElement,
|
||||
locator: loc,
|
||||
requireAction: loc.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: Self.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled)
|
||||
self.recursiveCallDebugLogs.append("HANDLER_DEBUG: searchResultCollectAll.logs.count = \(searchResultCollectAll.logs.count) before append for collectAll")
|
||||
|
||||
let searchResultCollectAll = self.search(element: startElement,
|
||||
locator: loc,
|
||||
requireAction: loc.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: AXMiscConstants.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &self.recursiveCallDebugLogs)
|
||||
self.recursiveCallDebugLogs.append(contentsOf: searchResultCollectAll.logs)
|
||||
self.recursiveCallDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_COLLECT_ALL")
|
||||
|
||||
if let locatedStartElement = searchResultCollectAll.foundElement {
|
||||
dLog("Locator found element: \(locatedStartElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)). This will be the root for collectAll recursion.")
|
||||
@ -187,7 +186,7 @@ extension AXorcist {
|
||||
var childrenRef: CFTypeRef?
|
||||
let childrenResult = AXUIElementCopyAttributeValue(
|
||||
axUIElement,
|
||||
kAXChildrenAttribute as CFString,
|
||||
AXAttributeNames.kAXChildrenAttribute as CFString,
|
||||
&childrenRef
|
||||
)
|
||||
|
||||
@ -215,7 +214,7 @@ extension AXorcist {
|
||||
|
||||
// Start recursion from the determined startElement
|
||||
if !self.recursiveCallDebugLogs.contains(where: { $0.contains("Failed to find element with provided locator criteria") && $0.contains("Cannot start collectAll") }) {
|
||||
// Only start if locator search (if any) didn't critically fail and try to return early.
|
||||
// Only start if locator search (if any) didn't critically fail and try to return early.
|
||||
collectRecursively(startElement.underlyingElement, 0)
|
||||
}
|
||||
|
||||
@ -229,4 +228,4 @@ extension AXorcist {
|
||||
)
|
||||
return encode(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
145
Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift
Normal file
145
Sources/AXorcist/Handlers/AXorcist+HandlerUtils.swift
Normal file
@ -0,0 +1,145 @@
|
||||
// AXorcist+HandlerUtils.swift - Common handler utilities
|
||||
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// MARK: - Handler Error Types
|
||||
internal struct HandlerResponseError: Error {
|
||||
let message: String
|
||||
let logs: [String]?
|
||||
|
||||
init(message: String, logs: [String]? = nil) {
|
||||
self.message = message
|
||||
self.logs = logs
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handler Utilities Extension
|
||||
extension AXorcist {
|
||||
|
||||
/// Finds a target element using path hints and locator criteria
|
||||
/// - Parameters:
|
||||
/// - appIdentifierOrNil: Application identifier (nil uses focused app)
|
||||
/// - locator: Optional locator criteria for finding the element
|
||||
/// - pathHint: Optional path hint for navigation from root
|
||||
/// - isRootedAtApp: If true, starts from application element; if false, uses baseElement
|
||||
/// - baseElement: Base element to start from (only used if isRootedAtApp is false)
|
||||
/// - maxDepthForSearch: Maximum search depth for locator searches
|
||||
/// - isDebugLoggingEnabled: Whether debug logging is enabled
|
||||
/// - currentDebugLogs: Debug logs array to append to
|
||||
/// - Returns: Result containing the found Element or HandlerResponseError
|
||||
@MainActor
|
||||
internal func findTargetElement(
|
||||
for appIdentifierOrNil: String?,
|
||||
locator: Locator?,
|
||||
pathHint: [String]?,
|
||||
isRootedAtApp: Bool = true,
|
||||
baseElement: Element? = nil,
|
||||
maxDepthForSearch: Int? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) async -> Result<Element, HandlerResponseError> {
|
||||
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: appIdentifierOrNil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
// Determine initial element
|
||||
let initialElement: Element
|
||||
if isRootedAtApp {
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[findTargetElement] Getting application element for: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "Failed to get application element for identifier: \(appIdentifier)"
|
||||
dLog("[findTargetElement] \(errorMessage)")
|
||||
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
|
||||
}
|
||||
initialElement = appElement
|
||||
} else {
|
||||
guard let providedBaseElement = baseElement else {
|
||||
let errorMessage = "Base element required when isRootedAtApp is false, but none provided"
|
||||
dLog("[findTargetElement] \(errorMessage)")
|
||||
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
|
||||
}
|
||||
initialElement = providedBaseElement
|
||||
}
|
||||
|
||||
var effectiveElement = initialElement
|
||||
|
||||
// Navigate using path hint if provided
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("[findTargetElement] Navigating with path_hint: \(pathHint.joined(separator: " -> ")) from root \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
|
||||
guard let navigatedElement = navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let lastLogBeforeDebug = currentDebugLogs.last
|
||||
let errorMessage: String
|
||||
if let lastLog = lastLogBeforeDebug, lastLog.contains("CRITICAL_NAV_PARSE_FAILURE_MARKER") {
|
||||
errorMessage = "Navigation parsing failed (critical marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else if let lastLog = lastLogBeforeDebug, lastLog.contains("CHILD_MATCH_FAILURE_MARKER") {
|
||||
errorMessage = "Navigation child match failed (child match marker found) for path hint: \(pathHint.joined(separator: " -> "))"
|
||||
} else {
|
||||
errorMessage = "Failed to navigate using path hint: \(pathHint.joined(separator: " -> "))"
|
||||
}
|
||||
|
||||
if isDebugLoggingEnabled {
|
||||
if let actualLastLog = lastLogBeforeDebug {
|
||||
dLog("[MARKER_CHECK] Checked lastLog for markers -> Error: '\(errorMessage)'. LastLog: '\(actualLastLog)'")
|
||||
} else {
|
||||
dLog("[MARKER_CHECK] currentDebugLogs was empty or lastLog was nil -> Error: '\(errorMessage)'")
|
||||
}
|
||||
}
|
||||
dLog("[findTargetElement] \(errorMessage)")
|
||||
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
|
||||
}
|
||||
effectiveElement = navigatedElement
|
||||
dLog("[findTargetElement] Successfully navigated path_hint. New effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
}
|
||||
|
||||
// Search using locator if provided
|
||||
if let actualLocator = locator {
|
||||
dLog("[findTargetElement] Locator provided. Searching from current effectiveElement: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)) using locator criteria: \(actualLocator.criteria)")
|
||||
|
||||
let searchResult = self.search(
|
||||
element: effectiveElement,
|
||||
locator: actualLocator,
|
||||
requireAction: actualLocator.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: maxDepthForSearch ?? AXMiscConstants.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
|
||||
dLog("[findTargetElement] Search completed. Logs from searchResult.logs count: \(searchResult.logs.count)")
|
||||
|
||||
guard let foundElement = searchResult.foundElement else {
|
||||
let errorMessage = "Search failed. Could not find element matching locator criteria \(actualLocator.criteria) starting from element \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))"
|
||||
if !currentDebugLogs.contains(errorMessage) {
|
||||
currentDebugLogs.append(errorMessage)
|
||||
}
|
||||
dLog("[findTargetElement] \(errorMessage)")
|
||||
return .failure(HandlerResponseError(message: errorMessage, logs: currentDebugLogs))
|
||||
}
|
||||
|
||||
dLog("[findTargetElement] Found element via locator: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
return .success(foundElement)
|
||||
} else {
|
||||
// No locator, use effective element after path hint navigation
|
||||
dLog("[findTargetElement] No locator provided. Using current effectiveElement as target: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
return .success(effectiveElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,9 +8,9 @@ import Foundation
|
||||
extension AXorcist {
|
||||
|
||||
// MARK: - handleQuery
|
||||
|
||||
|
||||
@MainActor
|
||||
internal func handleQuery(
|
||||
public func handleQuery(
|
||||
for appIdentifierOrNil: String?,
|
||||
locator: Locator,
|
||||
pathHint: [String]?,
|
||||
@ -20,7 +20,7 @@ extension AXorcist {
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
|
||||
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
|
||||
@ -41,7 +41,7 @@ extension AXorcist {
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
|
||||
if let navigatedElement = self.navigateToElement(
|
||||
if let navigatedElement = navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
@ -73,7 +73,7 @@ extension AXorcist {
|
||||
dLog(
|
||||
"Locator has root_element_path_hint: \(rootPathHint.joined(separator: " -> ")). Navigating from app element first."
|
||||
)
|
||||
guard let containerElement = self.navigateToElement(
|
||||
guard let containerElement = navigateToElement(
|
||||
from: appElement,
|
||||
pathHint: rootPathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
@ -94,18 +94,16 @@ extension AXorcist {
|
||||
"Searching with locator from element (determined by main path_hint or app root): \(searchStartElementForLocator.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))"
|
||||
)
|
||||
}
|
||||
|
||||
let searchResult = search(
|
||||
|
||||
let searchResult = self.search(
|
||||
element: searchStartElementForLocator,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: maxDepth ?? AXorcist.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
maxDepth: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for query")
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_QUERY")
|
||||
foundElement = searchResult.foundElement
|
||||
}
|
||||
|
||||
@ -114,7 +112,7 @@ extension AXorcist {
|
||||
elementToQuery,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
@ -122,7 +120,7 @@ extension AXorcist {
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
|
||||
let axElement = AXElement(attributes: attributes)
|
||||
return HandlerResponse(
|
||||
data: axElement,
|
||||
@ -139,9 +137,9 @@ extension AXorcist {
|
||||
}
|
||||
|
||||
// MARK: - handleGetAttributes
|
||||
|
||||
|
||||
@MainActor
|
||||
internal func handleGetAttributes(
|
||||
public func handleGetAttributes(
|
||||
for appIdentifierOrNil: String?,
|
||||
locator: Locator,
|
||||
requestedAttributes: [String]?,
|
||||
@ -151,100 +149,63 @@ extension AXorcist {
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
|
||||
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
|
||||
dLog("Handling get_attributes command for app: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
// Use findTargetElement to get the target element
|
||||
let targetElementResult = await self.findTargetElement(
|
||||
for: appIdentifier,
|
||||
locator: locator,
|
||||
pathHint: pathHint,
|
||||
maxDepthForSearch: maxDepth ?? AXMiscConstants.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "Application not found: \(appIdentifier)"
|
||||
dLog("handleGetAttributes: \(errorMessage)")
|
||||
)
|
||||
|
||||
let foundElement: Element
|
||||
switch targetElementResult {
|
||||
case .failure(let errorData):
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: errorMessage,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
error: errorData.message,
|
||||
debug_logs: errorData.logs ?? currentDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("handleGetAttributes: Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
|
||||
if let navigatedElement = self.navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
effectiveElement = navigatedElement
|
||||
} else {
|
||||
let errorMessage = "Element not found via path hint: \(pathHint.joined(separator: " -> "))"
|
||||
dLog("handleGetAttributes: \(errorMessage)")
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: errorMessage,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
)
|
||||
}
|
||||
case .success(let element):
|
||||
foundElement = element
|
||||
}
|
||||
|
||||
dLog(
|
||||
"handleGetAttributes: Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))"
|
||||
"handleGetAttributes: Element found: \(foundElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)). Fetching attributes: \(requestedAttributes ?? ["all"])..."
|
||||
)
|
||||
let searchResult = search(
|
||||
element: effectiveElement,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: maxDepth ?? AXorcist.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for getAttributes")
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_GET_ATTRIBUTES")
|
||||
let foundElement = searchResult.foundElement
|
||||
|
||||
if let elementToQuery = foundElement {
|
||||
dLog(
|
||||
"handleGetAttributes: Element found: \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)). Fetching attributes: \(requestedAttributes ?? ["all"])..."
|
||||
)
|
||||
var attributes = getElementAttributes(
|
||||
elementToQuery,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
dLog(
|
||||
"Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))."
|
||||
)
|
||||
|
||||
let axElement = AXElement(attributes: attributes)
|
||||
return HandlerResponse(
|
||||
data: axElement,
|
||||
error: nil,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
)
|
||||
} else {
|
||||
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
|
||||
dLog("handleGetAttributes: \(errorMessage)")
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: errorMessage,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
)
|
||||
let elementToQuery = foundElement
|
||||
var attributes = getElementAttributes(
|
||||
elementToQuery,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
dLog(
|
||||
"Successfully fetched attributes for element \(elementToQuery.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))."
|
||||
)
|
||||
|
||||
let axElement = AXElement(attributes: attributes)
|
||||
return HandlerResponse(
|
||||
data: axElement,
|
||||
error: nil,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
public func handleDescribeElement(
|
||||
for appIdentifierOrNil: String?,
|
||||
@ -256,12 +217,41 @@ extension AXorcist {
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
|
||||
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? self.focusedAppKeyValue
|
||||
dLog("Handling describe_element for app: \(appIdentifier)")
|
||||
|
||||
let searchMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
|
||||
|
||||
// Use findTargetElement to get the target element
|
||||
let targetElementResult = await self.findTargetElement(
|
||||
for: appIdentifier,
|
||||
locator: locator,
|
||||
pathHint: pathHint,
|
||||
maxDepthForSearch: searchMaxDepth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
let elementToDescribe: Element
|
||||
switch targetElementResult {
|
||||
case .failure(let errorData):
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: errorData.message,
|
||||
debug_logs: errorData.logs ?? currentDebugLogs
|
||||
)
|
||||
case .success(let element):
|
||||
elementToDescribe = element
|
||||
}
|
||||
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Element found: \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)). Now describing."
|
||||
)
|
||||
|
||||
// Get application element for path generation
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
@ -274,59 +264,11 @@ extension AXorcist {
|
||||
)
|
||||
}
|
||||
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
dLog("handleDescribeElement: Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
|
||||
if let navigatedElement = self.navigateToElement(
|
||||
from: appElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
effectiveElement = navigatedElement
|
||||
} else {
|
||||
let errorMessage = "Element not found via path hint for describe: \(pathHint.joined(separator: " -> "))"
|
||||
dLog("handleDescribeElement: \(errorMessage)")
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: errorMessage,
|
||||
debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Searching for element to describe using locator: \(locator.criteria) from effective element: \(effectiveElement.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))"
|
||||
)
|
||||
|
||||
let searchMaxDepth = maxDepth ?? AXorcist.defaultMaxDepthSearch
|
||||
|
||||
let searchResult = search(
|
||||
element: effectiveElement,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
depth: 0,
|
||||
maxDepth: searchMaxDepth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
currentDebugLogs.append("HANDLER_DEBUG: searchResult.logs.count = \(searchResult.logs.count) before append for describeElement")
|
||||
currentDebugLogs.append(contentsOf: searchResult.logs)
|
||||
currentDebugLogs.append("POST_SEARCH_LOG_APPEND_MARKER_IN_DESCRIBE")
|
||||
guard let elementToDescribe = searchResult.foundElement else {
|
||||
let error = "[AXorcist.handleDescribeElement] Element to describe not found for locator: \(locator.criteria)"
|
||||
currentDebugLogs.append(error)
|
||||
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
|
||||
}
|
||||
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Element found: \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)). Now describing."
|
||||
)
|
||||
|
||||
var attributes = getElementAttributes(
|
||||
elementToDescribe,
|
||||
requestedAttributes: requestedAttributes ?? ["all"],
|
||||
forMultiDefault: true,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
requestedAttributes: requestedAttributes ?? ["all"],
|
||||
forMultiDefault: true,
|
||||
targetRole: locator.criteria[AXAttributeNames.kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .verbose,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
@ -339,7 +281,7 @@ extension AXorcist {
|
||||
attributes: attributes,
|
||||
path: elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
)
|
||||
|
||||
|
||||
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,408 +0,0 @@
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// MARK: - Query Handler Methods Extension
|
||||
extension AXorcist {
|
||||
|
||||
// Handle getting attributes for a specific element using locator
|
||||
@MainActor
|
||||
public func handleGetAttributes(
|
||||
for appIdentifierOrNil: String? = nil,
|
||||
locator: Locator,
|
||||
requestedAttributes: [String]? = nil,
|
||||
pathHint: [String]? = nil,
|
||||
maxDepth: Int? = nil,
|
||||
outputFormat: OutputFormat? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> HandlerResponse {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[AXorcist.handleGetAttributes] Handling for app: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "Application not found: \(appIdentifier)"
|
||||
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
|
||||
// Find element to get attributes from
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
let pathHintString = pathHint.joined(separator: " -> ")
|
||||
_ = pathHintString // Silences compiler warning
|
||||
let logMessage = "[AXorcist.handleGetAttributes] Navigating with path_hint: \(pathHintString)"
|
||||
dLog(logMessage)
|
||||
if let navigatedElement = navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
effectiveElement = navigatedElement
|
||||
} else {
|
||||
let pathHintStringForError = pathHint.joined(separator: " -> ")
|
||||
_ = pathHintStringForError // Silences compiler warning
|
||||
let errorMessageText = "Element not found via path hint: \(pathHintStringForError)"
|
||||
dLog("[AXorcist.handleGetAttributes] \(errorMessageText)")
|
||||
return HandlerResponse(data: nil, error: errorMessageText, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
var elementToQuery: Element?
|
||||
let axApplicationKey = "AXApplication" // String literal for the attribute key
|
||||
|
||||
if locator.criteria.count == 1, let appCritervalue = locator.criteria[axApplicationKey],
|
||||
(appCritervalue.uppercased() == "YES" || appCritervalue.uppercased() == "TRUE") {
|
||||
let briefDesc = effectiveElement.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog(
|
||||
"[AXorcist.handleGetAttributes] Locator criteria is {'\(axApplicationKey)': '\(appCritervalue)'}. Using effectiveElement (\(briefDesc)) as target."
|
||||
)
|
||||
elementToQuery = effectiveElement
|
||||
} else {
|
||||
let rootElementDescription = effectiveElement.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
dLog(
|
||||
"[AXorcist.handleGetAttributes] Not an AXApplication-only locator or value mismatch. Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)"
|
||||
)
|
||||
let searchResultGetAttributes = search(
|
||||
element: effectiveElement,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: searchResultGetAttributes.logs)
|
||||
elementToQuery = searchResultGetAttributes.foundElement
|
||||
}
|
||||
|
||||
if let actualElementToQuery = elementToQuery {
|
||||
let elementDescription = actualElementToQuery.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
let attributesDescription = (requestedAttributes ?? ["all"]).description
|
||||
dLog(
|
||||
"[AXorcist.handleGetAttributes] Element identified/found: \(elementDescription). Fetching attributes: \(attributesDescription)..."
|
||||
)
|
||||
|
||||
var attributes = getElementAttributes(
|
||||
actualElementToQuery,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = actualElementToQuery.generatePathArray(
|
||||
upTo: appElement,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
let axElement = AXElement(attributes: attributes, path: elementPathArray)
|
||||
|
||||
dLog(
|
||||
"[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(actualElementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))."
|
||||
)
|
||||
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
|
||||
} else {
|
||||
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
|
||||
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle query command - find an element matching criteria
|
||||
@MainActor
|
||||
public func handleQuery(
|
||||
for appIdentifierOrNil: String? = nil,
|
||||
locator: Locator,
|
||||
pathHint: [String]? = nil,
|
||||
maxDepth: Int? = nil,
|
||||
requestedAttributes: [String]? = nil,
|
||||
outputFormat: OutputFormat? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> HandlerResponse {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[AXorcist.handleQuery] Handling query for app: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "Application not found: \(appIdentifier)"
|
||||
dLog("[AXorcist.handleQuery] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
let pathHintString = pathHint.joined(separator: " -> ")
|
||||
_ = pathHintString // Silences compiler warning
|
||||
dLog("[AXorcist.handleQuery] Navigating with path_hint: \(pathHintString)")
|
||||
if let navigatedElement = navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
effectiveElement = navigatedElement
|
||||
} else {
|
||||
let errorMessage = "Element not found via path hint: \(pathHintString)"
|
||||
dLog("[AXorcist.handleQuery] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is an app-only locator (only application/bundle_id/pid/path criteria)
|
||||
let appSpecifiers = ["application", "bundle_id", "pid", "path"]
|
||||
let criteriaKeys = locator.criteria.keys
|
||||
let isAppOnlyLocator = criteriaKeys.allSatisfy { appSpecifiers.contains($0) } && criteriaKeys.count == 1
|
||||
|
||||
var foundElement: Element?
|
||||
|
||||
if isAppOnlyLocator {
|
||||
dLog(
|
||||
"[AXorcist.handleQuery] Locator is app-only (criteria: \(locator.criteria)). Using appElement directly."
|
||||
)
|
||||
foundElement = effectiveElement
|
||||
} else {
|
||||
dLog("[AXorcist.handleQuery] Locator contains element-specific criteria. Proceeding with search.")
|
||||
var searchStartElementForLocator = appElement
|
||||
|
||||
if let rootPathHint = locator.root_element_path_hint, !rootPathHint.isEmpty {
|
||||
let rootPathHintString = rootPathHint.joined(separator: " -> ")
|
||||
_ = rootPathHintString // Silences compiler warning
|
||||
dLog(
|
||||
"[AXorcist.handleQuery] Locator has root_element_path_hint: \(rootPathHintString). Navigating from app element first."
|
||||
)
|
||||
guard let containerElement = navigateToElement(
|
||||
from: appElement,
|
||||
pathHint: rootPathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage =
|
||||
"Container for locator not found via root_element_path_hint: \(rootPathHintString)"
|
||||
dLog("[AXorcist.handleQuery] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
searchStartElementForLocator = containerElement
|
||||
let containerDescription = searchStartElementForLocator.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
_ = containerDescription // Silences compiler warning
|
||||
dLog(
|
||||
"[AXorcist.handleQuery] Searching with locator within container found by root_element_path_hint: \(containerDescription)"
|
||||
)
|
||||
} else {
|
||||
searchStartElementForLocator = effectiveElement
|
||||
let searchDescription = searchStartElementForLocator.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
_ = searchDescription // Silences compiler warning
|
||||
dLog(
|
||||
"[AXorcist.handleQuery] Searching with locator from element (determined by main path_hint or app root): \(searchDescription)"
|
||||
)
|
||||
}
|
||||
|
||||
let finalSearchTarget = (pathHint != nil && !pathHint!.isEmpty) ? effectiveElement :
|
||||
searchStartElementForLocator
|
||||
|
||||
let searchResultQuery = search(
|
||||
element: finalSearchTarget,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: searchResultQuery.logs)
|
||||
foundElement = searchResultQuery.foundElement
|
||||
}
|
||||
|
||||
if let elementToQuery = foundElement {
|
||||
let elementDescription = elementToQuery.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
_ = elementDescription // Silences compiler warning
|
||||
dLog("[AXorcist.handleQuery] Element found: \(elementDescription). Fetching attributes...")
|
||||
|
||||
var attributes = getElementAttributes(
|
||||
elementToQuery,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = elementToQuery.generatePathArray(
|
||||
upTo: appElement,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
let axElement = AXElement(attributes: attributes, path: elementPathArray)
|
||||
|
||||
dLog("[AXorcist.handleQuery] Successfully found and processed element with query.")
|
||||
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
|
||||
} else {
|
||||
let errorMessage = "No element matches query criteria with locator: \(String(describing: locator))"
|
||||
dLog("[AXorcist.handleQuery] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle describe element command - provides comprehensive details about a specific element
|
||||
@MainActor
|
||||
public func handleDescribeElement(
|
||||
for appIdentifierOrNil: String? = nil,
|
||||
locator: Locator,
|
||||
pathHint: [String]? = nil,
|
||||
maxDepth: Int? = nil,
|
||||
outputFormat: OutputFormat? = nil,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> HandlerResponse {
|
||||
func dLog(_ message: String) {
|
||||
if isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(message)
|
||||
}
|
||||
}
|
||||
|
||||
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
|
||||
dLog("[AXorcist.handleDescribeElement] Handling for app: \(appIdentifier)")
|
||||
|
||||
guard let appElement = applicationElement(
|
||||
for: appIdentifier,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
let errorMessage = "Application not found: \(appIdentifier)"
|
||||
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
|
||||
var effectiveElement = appElement
|
||||
if let pathHint = pathHint, !pathHint.isEmpty {
|
||||
let pathHintString = pathHint.joined(separator: " -> ")
|
||||
_ = pathHintString // Silences compiler warning
|
||||
dLog("[AXorcist.handleDescribeElement] Navigating with path_hint: \(pathHintString)")
|
||||
if let navigatedElement = navigateToElement(
|
||||
from: effectiveElement,
|
||||
pathHint: pathHint,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
effectiveElement = navigatedElement
|
||||
} else {
|
||||
let errorMessage = "Element not found via path hint for describe_element: \(pathHintString)"
|
||||
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
let rootElementDescription = effectiveElement.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
_ = rootElementDescription // Silences compiler warning
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)"
|
||||
)
|
||||
let searchResultDescribe = search(
|
||||
element: effectiveElement,
|
||||
locator: locator,
|
||||
requireAction: locator.requireAction,
|
||||
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
)
|
||||
currentDebugLogs.append(contentsOf: searchResultDescribe.logs)
|
||||
let foundElementForDescribe = searchResultDescribe.foundElement
|
||||
|
||||
if let elementToDescribe = foundElementForDescribe {
|
||||
let elementDescription = elementToDescribe.briefDescription(
|
||||
option: ValueFormatOption.default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
_ = elementDescription // Silences compiler warning
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Element found: \(elementDescription). Describing with verbose output..."
|
||||
)
|
||||
|
||||
// For describe_element, we typically want ALL attributes with verbose output
|
||||
var attributes = getElementAttributes(
|
||||
elementToDescribe,
|
||||
requestedAttributes: [], // Empty means 'all standard' or 'all known'
|
||||
forMultiDefault: true,
|
||||
targetRole: locator.criteria[kAXRoleAttribute],
|
||||
outputFormat: outputFormat ?? .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = elementToDescribe.generatePathArray(
|
||||
upTo: appElement,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
let axElement = AXElement(attributes: attributes, path: elementPathArray)
|
||||
|
||||
dLog(
|
||||
"[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: ValueFormatOption.default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))."
|
||||
)
|
||||
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
|
||||
} else {
|
||||
let errorMessage = "No element found for describe_element with locator: \(String(describing: locator))"
|
||||
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
|
||||
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Sources/AXorcist/Search/AttributeFormatter.swift
Normal file
67
Sources/AXorcist/Search/AttributeFormatter.swift
Normal file
@ -0,0 +1,67 @@
|
||||
// AttributeFormatter.swift - Contains functions for formatting element attributes for display
|
||||
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Note: This file assumes Element, ValueFormatOption, and AXMiscConstants.kAXNotAvailableString are available.
|
||||
// formatAXValue is assumed to be available from elsewhere (e.g., ValueFormatter.swift)
|
||||
|
||||
// Helper function to format the parent attribute
|
||||
@MainActor
|
||||
internal func formatParentAttribute(_ parent: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let parentElement = parent else { return AnyCodable(nil as String?) }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Element: \(parentElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
|
||||
} else {
|
||||
return AnyCodable(parentElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format the children attribute
|
||||
@MainActor
|
||||
internal func formatChildrenAttribute(_ children: [Element]?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let actualChildren = children, !actualChildren.isEmpty else { return AnyCodable("[]") }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Array of \(actualChildren.count) Element(s)")
|
||||
} else if outputFormat == .verbose {
|
||||
var childrenSummaries: [String] = []
|
||||
for childElement in actualChildren {
|
||||
childrenSummaries.append(childElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
|
||||
} else { // .smart output
|
||||
return AnyCodable("Array of \(actualChildren.count) children")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format the focused UI element attribute
|
||||
@MainActor
|
||||
internal func formatFocusedUIElementAttribute(_ focusedElement: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let actualFocusedElement = focusedElement else { return AnyCodable(nil as String?) }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Element: \(actualFocusedElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
|
||||
} else {
|
||||
return AnyCodable(actualFocusedElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format a raw CFTypeRef for .text_content output
|
||||
@MainActor
|
||||
internal func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) -> String {
|
||||
guard let value = rawValue else { return AXMiscConstants.kAXNotAvailableString } // AXMiscConstants.kAXNotAvailableString needs to be defined
|
||||
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
|
||||
// Assuming formatAXValue is globally available or accessible.
|
||||
// If it's in a specific file like ValueFormatter.swift, direct call might need qualification
|
||||
// or this function might need to be part of that file/extension.
|
||||
// For now, assume it's callable like this.
|
||||
return formatAXValue(axVal, option: .default) // Assumes formatAXValue returns String and ValueFormatOption.default is valid here
|
||||
} else if typeID == CFNumberGetTypeID() { return (value as! NSNumber).stringValue } else if typeID == CFBooleanGetTypeID() { return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false" } else { return "<\(CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType")>" }
|
||||
}
|
||||
@ -35,40 +35,40 @@ private func extractDirectPropertyValue(for attributeName: String, from element:
|
||||
|
||||
// Ensure logging parameters are passed to Element methods
|
||||
switch attributeName {
|
||||
case kAXPathHintAttribute:
|
||||
extractedValue = element.attribute(Attribute<String>(kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case kAXRoleAttribute:
|
||||
case AXAttributeNames.kAXPathHintAttribute:
|
||||
extractedValue = element.attribute(Attribute<String>(AXAttributeNames.kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case AXAttributeNames.kAXRoleAttribute:
|
||||
extractedValue = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case kAXSubroleAttribute:
|
||||
case AXAttributeNames.kAXSubroleAttribute:
|
||||
extractedValue = element.subrole(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case kAXTitleAttribute:
|
||||
case AXAttributeNames.kAXTitleAttribute:
|
||||
extractedValue = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case kAXDescriptionAttribute:
|
||||
case AXAttributeNames.kAXDescriptionAttribute:
|
||||
extractedValue = element.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
case kAXEnabledAttribute:
|
||||
case AXAttributeNames.kAXEnabledAttribute:
|
||||
let val = element.isEnabled(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
|
||||
case kAXFocusedAttribute:
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
|
||||
case AXAttributeNames.kAXFocusedAttribute:
|
||||
let val = element.isFocused(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
|
||||
case kAXHiddenAttribute:
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
|
||||
case AXAttributeNames.kAXHiddenAttribute:
|
||||
let val = element.isHidden(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
|
||||
case isIgnoredAttributeKey:
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
|
||||
case AXMiscConstants.isIgnoredAttributeKey:
|
||||
let val = element.isIgnored(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val ? "true" : "false" }
|
||||
case "PID":
|
||||
let val = element.pid(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
|
||||
case kAXElementBusyAttribute:
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
|
||||
case AXAttributeNames.kAXElementBusyAttribute:
|
||||
let val = element.isElementBusy(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
extractedValue = val
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
|
||||
if outputFormat == .text_content { extractedValue = val?.description ?? AXMiscConstants.kAXNotAvailableString }
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
@ -81,9 +81,9 @@ private func determineAttributesToFetch(requestedAttributes: [String], forMultiD
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var attributesToFetch = requestedAttributes
|
||||
if forMultiDefault {
|
||||
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXTitleAttribute, kAXIdentifierAttribute]
|
||||
if let role = targetRole, role == kAXStaticTextRole {
|
||||
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXIdentifierAttribute]
|
||||
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 {
|
||||
var attrNames: CFArray?
|
||||
@ -115,19 +115,19 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
|
||||
|
||||
for attr in attributesToFetch {
|
||||
var tempCallLogs: [String] = [] // Logs for a specific attribute fetching call
|
||||
if attr == kAXParentAttribute {
|
||||
if attr == AXAttributeNames.kAXParentAttribute {
|
||||
tempCallLogs.removeAll()
|
||||
let parent = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
|
||||
result[kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatParentAttribute will manage its own logs now
|
||||
result[AXAttributeNames.kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatParentAttribute will manage its own logs now
|
||||
currentDebugLogs.append(contentsOf: tempCallLogs) // Collect logs from element.parent and formatParentAttribute
|
||||
continue
|
||||
} else if attr == kAXChildrenAttribute {
|
||||
} else if attr == AXAttributeNames.kAXChildrenAttribute {
|
||||
tempCallLogs.removeAll()
|
||||
let children = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
|
||||
result[attr] = formatChildrenAttribute(children, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // Directly assign AnyCodable
|
||||
currentDebugLogs.append(contentsOf: tempCallLogs)
|
||||
continue
|
||||
} else if attr == kAXFocusedUIElementAttribute {
|
||||
} else if attr == AXAttributeNames.kAXFocusedUIElementAttribute {
|
||||
tempCallLogs.removeAll()
|
||||
let focused = element.focusedElement(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
|
||||
result[attr] = formatFocusedUIElementAttribute(focused, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // Directly assign AnyCodable
|
||||
@ -142,7 +142,6 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
|
||||
|
||||
if wasHandledDirectly {
|
||||
finalValueToStore = directValue
|
||||
dLog("Attribute '\(attr)' handled directly, value: \(String(describing: directValue))")
|
||||
} else {
|
||||
tempCallLogs.removeAll()
|
||||
let rawCFValue: CFTypeRef? = element.rawAttributeValue(named: attr, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
|
||||
@ -152,13 +151,11 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
|
||||
} else {
|
||||
finalValueToStore = formatCFTypeRef(rawCFValue, option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
dLog("Attribute '\(attr)' fetched via rawAttributeValue, formatted value: \(String(describing: finalValueToStore))")
|
||||
}
|
||||
|
||||
if outputFormat == .smart {
|
||||
if let strVal = finalValueToStore as? String,
|
||||
strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == kAXNotAvailableString {
|
||||
dLog("Smart format: Skipping attribute '\(attr)' with unhelpful value: \(strVal)")
|
||||
strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == AXMiscConstants.kAXNotAvailableString {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -166,36 +163,32 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
|
||||
}
|
||||
|
||||
tempLogs.removeAll()
|
||||
if result[computedNameAttributeKey] == nil {
|
||||
if result[AXMiscConstants.computedNameAttributeKey] == nil {
|
||||
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
result[computedNameAttributeKey] = AnyCodable(name)
|
||||
dLog("Added ComputedName: \(name)")
|
||||
result[AXMiscConstants.computedNameAttributeKey] = AnyCodable(name)
|
||||
}
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
if result[isClickableAttributeKey] == nil {
|
||||
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
|
||||
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
if result[AXMiscConstants.isClickableAttributeKey] == nil {
|
||||
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == AXRoleNames.kAXButtonRole)
|
||||
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
if isButton || hasPressAction {
|
||||
result[isClickableAttributeKey] = AnyCodable(true)
|
||||
dLog("Added IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
|
||||
result[AXMiscConstants.isClickableAttributeKey] = AnyCodable(true)
|
||||
}
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
if outputFormat == .verbose && result[computedPathAttributeKey] == nil {
|
||||
if outputFormat == .verbose && result[AXMiscConstants.computedPathAttributeKey] == nil {
|
||||
let path = element.generatePathString(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
result[computedPathAttributeKey] = AnyCodable(path)
|
||||
dLog("Added ComputedPath (verbose): \(path)")
|
||||
result[AXMiscConstants.computedPathAttributeKey] = AnyCodable(path)
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
populateActionNamesAttribute(for: element, result: &result, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
dLog("getElementAttributes finished. Result keys: \(result.keys.joined(separator: ", "))")
|
||||
return result
|
||||
}
|
||||
|
||||
@ -203,8 +196,7 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
|
||||
private func populateActionNamesAttribute(for element: Element, result: inout ElementAttributes, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
if result[kAXActionNamesAttribute] != nil {
|
||||
dLog("populateActionNamesAttribute: Already present or explicitly requested, skipping.")
|
||||
if result[AXAttributeNames.kAXActionNamesAttribute] != nil {
|
||||
return
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Appending potentially empty tempLogs, for consistency, though it does nothing here.
|
||||
@ -213,92 +205,38 @@ private func populateActionNamesAttribute(for element: Element, result: inout El
|
||||
tempLogs.removeAll()
|
||||
if let currentActions = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !currentActions.isEmpty {
|
||||
actionsToStore = currentActions
|
||||
dLog("populateActionNamesAttribute: Got \(currentActions.count) from supportedActions.")
|
||||
} else {
|
||||
dLog("populateActionNamesAttribute: supportedActions was nil or empty. Trying kAXActionsAttribute.")
|
||||
tempLogs.removeAll() // Clear before next call that uses it
|
||||
if let fallbackActions: [String] = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !fallbackActions.isEmpty {
|
||||
if let fallbackActions: [String] = element.attribute(Attribute<[String]>(AXAttributeNames.kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !fallbackActions.isEmpty {
|
||||
actionsToStore = fallbackActions
|
||||
dLog("populateActionNamesAttribute: Got \(fallbackActions.count) from kAXActionsAttribute fallback.")
|
||||
}
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let pressActionSupported = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
let pressActionSupported = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
dLog("populateActionNamesAttribute: kAXPressAction supported: \(pressActionSupported).")
|
||||
if pressActionSupported {
|
||||
if actionsToStore == nil { actionsToStore = [kAXPressAction] } else if !actionsToStore!.contains(kAXPressAction) { actionsToStore!.append(kAXPressAction) }
|
||||
if actionsToStore == nil { actionsToStore = [AXActionNames.kAXPressAction] } else if !actionsToStore!.contains(AXActionNames.kAXPressAction) { actionsToStore!.append(AXActionNames.kAXPressAction) }
|
||||
}
|
||||
|
||||
if let finalActions = actionsToStore, !finalActions.isEmpty {
|
||||
result[kAXActionNamesAttribute] = AnyCodable(finalActions)
|
||||
dLog("populateActionNamesAttribute: Final actions: \(finalActions.joined(separator: ", ")).")
|
||||
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable(finalActions)
|
||||
} else {
|
||||
tempLogs.removeAll()
|
||||
let primaryNil = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
tempLogs.removeAll()
|
||||
let fallbackNil = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
|
||||
let fallbackNil = element.attribute(Attribute<[String]>(AXAttributeNames.kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
if primaryNil && fallbackNil && !pressActionSupported {
|
||||
result[kAXActionNamesAttribute] = AnyCodable(kAXNotAvailableString)
|
||||
dLog("populateActionNamesAttribute: All action sources nil/unsupported. Set to kAXNotAvailableString.")
|
||||
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable(AXMiscConstants.kAXNotAvailableString)
|
||||
} else {
|
||||
result[kAXActionNamesAttribute] = AnyCodable("\(kAXNotAvailableString) (no specific actions found or list empty)")
|
||||
dLog("populateActionNamesAttribute: Some action source present but list empty. Set to verbose kAXNotAvailableString.")
|
||||
result[AXAttributeNames.kAXActionNamesAttribute] = AnyCodable("\(AXMiscConstants.kAXNotAvailableString) (no specific actions found or list empty)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Attribute Formatting Helpers
|
||||
|
||||
// Helper function to format the parent attribute
|
||||
@MainActor
|
||||
private func formatParentAttribute(_ parent: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let parentElement = parent else { return AnyCodable(nil as String?) }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Element: \(parentElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
|
||||
} else {
|
||||
return AnyCodable(parentElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format the children attribute
|
||||
@MainActor
|
||||
private func formatChildrenAttribute(_ children: [Element]?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let actualChildren = children, !actualChildren.isEmpty else { return AnyCodable("[]") }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Array of \(actualChildren.count) Element(s)")
|
||||
} else if outputFormat == .verbose {
|
||||
var childrenSummaries: [String] = []
|
||||
for childElement in actualChildren {
|
||||
childrenSummaries.append(childElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
|
||||
} else { // .smart output
|
||||
return AnyCodable("Array of \(actualChildren.count) children")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format the focused UI element attribute
|
||||
@MainActor
|
||||
private func formatFocusedUIElementAttribute(_ focusedElement: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
guard let actualFocusedElement = focusedElement else { return AnyCodable(nil as String?) }
|
||||
if outputFormat == .text_content {
|
||||
return AnyCodable("Element: \(actualFocusedElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
|
||||
} else {
|
||||
return AnyCodable(actualFocusedElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes the given ElementAttributes dictionary into a new dictionary containing
|
||||
/// a single key "json_representation" with the JSON string as its value.
|
||||
/// If encoding fails, returns a dictionary with an error message.
|
||||
@ -328,26 +266,23 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
|
||||
var attributes: ElementAttributes = [:]
|
||||
|
||||
tempLogs.removeAll()
|
||||
dLog("getComputedAttributes for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))")
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
attributes[computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
|
||||
dLog("ComputedName: \(name)")
|
||||
attributes[AXMiscConstants.computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
|
||||
}
|
||||
currentDebugLogs.append(contentsOf: tempLogs)
|
||||
|
||||
tempLogs.removeAll()
|
||||
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
|
||||
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == AXRoleNames.kAXButtonRole)
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from role call
|
||||
tempLogs.removeAll()
|
||||
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
let hasPressAction = element.isActionSupported(AXActionNames.kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from isActionSupported call
|
||||
|
||||
if isButton || hasPressAction {
|
||||
attributes[isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
|
||||
dLog("IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
|
||||
attributes[AXMiscConstants.isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
|
||||
}
|
||||
|
||||
// Ensure other computed attributes like ComputedPath also use methods with logging if they exist.
|
||||
@ -358,15 +293,5 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
|
||||
|
||||
// MARK: - Attribute Formatting Helpers (Additional)
|
||||
|
||||
// Helper function to format a raw CFTypeRef for .text_content output
|
||||
@MainActor
|
||||
private func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) -> String {
|
||||
guard let value = rawValue else { return 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: .default) // Assumes formatAXValue returns String
|
||||
} else if typeID == CFNumberGetTypeID() { return (value as! NSNumber).stringValue } else if typeID == CFBooleanGetTypeID() { return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false" } else { return "<\(CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType")>" }
|
||||
}
|
||||
|
||||
// Any other attribute-specific helper functions could go here in the future.
|
||||
// Formatting functions have been moved to AttributeFormatter.swift
|
||||
// This includes: formatParentAttribute, formatChildrenAttribute, formatFocusedUIElementAttribute, formatRawCFValueForTextContent
|
||||
|
||||
@ -22,8 +22,8 @@ internal func attributesMatch(
|
||||
|
||||
if !matchComputedNameAttributes(
|
||||
element: element,
|
||||
computedNameEquals: matchDetails[computedNameAttributeKey + "_equals"],
|
||||
computedNameContains: matchDetails[computedNameAttributeKey + "_contains"],
|
||||
computedNameEquals: matchDetails[AXMiscConstants.computedNameAttributeKey + "_equals"],
|
||||
computedNameContains: matchDetails[AXMiscConstants.computedNameAttributeKey + "_contains"],
|
||||
depth: depth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
@ -32,12 +32,12 @@ internal func attributesMatch(
|
||||
}
|
||||
|
||||
for (key, expectedValue) in matchDetails {
|
||||
if key == computedNameAttributeKey + "_equals" || key == computedNameAttributeKey + "_contains" { continue }
|
||||
if key == AXMiscConstants.computedNameAttributeKey + "_equals" || key == AXMiscConstants.computedNameAttributeKey + "_contains" { continue }
|
||||
if key ==
|
||||
kAXRoleAttribute { continue } // Already handled by ElementSearch's role check or not a primary filter here
|
||||
AXAttributeNames.kAXRoleAttribute { continue } // Already handled by ElementSearch's role check or not a primary filter here
|
||||
|
||||
if key == kAXEnabledAttribute || key == kAXFocusedAttribute || key == kAXHiddenAttribute || key ==
|
||||
kAXElementBusyAttribute || key == isIgnoredAttributeKey || key == kAXMainAttribute {
|
||||
if key == AXAttributeNames.kAXEnabledAttribute || key == AXAttributeNames.kAXFocusedAttribute || key == AXAttributeNames.kAXHiddenAttribute || key ==
|
||||
AXAttributeNames.kAXElementBusyAttribute || key == AXMiscConstants.isIgnoredAttributeKey || key == AXAttributeNames.kAXMainAttribute {
|
||||
if !matchBooleanAttribute(
|
||||
element: element,
|
||||
key: key,
|
||||
@ -51,7 +51,7 @@ internal func attributesMatch(
|
||||
continue
|
||||
}
|
||||
|
||||
if key == kAXActionNamesAttribute || key == kAXAllowedValuesAttribute || key == kAXChildrenAttribute {
|
||||
if key == AXAttributeNames.kAXActionNamesAttribute || key == AXAttributeNames.kAXAllowedValuesAttribute || key == AXAttributeNames.kAXChildrenAttribute {
|
||||
if !matchArrayAttribute(
|
||||
element: element,
|
||||
key: key,
|
||||
@ -80,225 +80,3 @@ internal func attributesMatch(
|
||||
dLog("attributesMatch [D\(depth)]: All attributes MATCHED criteria.")
|
||||
return true
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchStringAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
if let currentValue = element.attribute(
|
||||
Attribute<String>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
) {
|
||||
if currentValue != expectedValueString {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' expected '\(expectedValueString)', but found '\(currentValue)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if expectedValueString
|
||||
.lowercased() == "nil" || expectedValueString == kAXNotAvailableString || expectedValueString.isEmpty {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' not found, but expected value ('\(expectedValueString)') suggests absence is OK. Match for this key."
|
||||
)
|
||||
return true
|
||||
} else {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' (expected '\(expectedValueString)') not found or not convertible to String. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchArrayAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
guard let expectedArray = decodeExpectedArray(
|
||||
fromString: expectedValueString,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
var actualArray: [String]?
|
||||
if key == kAXActionNamesAttribute {
|
||||
actualArray = element.supportedActions(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
} else if key == kAXAllowedValuesAttribute {
|
||||
actualArray = element.attribute(
|
||||
Attribute<[String]>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
} else if key == kAXChildrenAttribute {
|
||||
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?
|
||||
.map { childElement -> String in
|
||||
var childLogs: [String] = []
|
||||
return childElement
|
||||
.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
|
||||
}
|
||||
} else {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Unknown array key '\(key)'. This function needs to be extended for this key."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if let actual = actualArray {
|
||||
if Set(actual) != Set(expectedArray) {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' expected '\(expectedArray)', but found '\(actual)'. Sets differ. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if expectedArray.isEmpty {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' not found, but expected array was empty. Match for this key."
|
||||
)
|
||||
return true
|
||||
}
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchBooleanAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
var currentBoolValue: Bool?
|
||||
|
||||
switch key {
|
||||
case kAXEnabledAttribute: currentBoolValue = element.isEnabled(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case kAXFocusedAttribute: currentBoolValue = element.isFocused(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case kAXHiddenAttribute: currentBoolValue = element.isHidden(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case kAXElementBusyAttribute: currentBoolValue = element.isElementBusy(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case isIgnoredAttributeKey: currentBoolValue = element.isIgnored(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case kAXMainAttribute: currentBoolValue = element.attribute(
|
||||
Attribute<Bool>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
default:
|
||||
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
|
||||
return false
|
||||
}
|
||||
|
||||
if let actualBool = currentBoolValue {
|
||||
let expectedBool = expectedValueString.lowercased() == "true"
|
||||
if actualBool != expectedBool {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' expected '\(expectedBool)', but found '\(actualBool)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchComputedNameAttributes(
|
||||
element: Element,
|
||||
computedNameEquals: String?,
|
||||
computedNameContains: String?,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
if computedNameEquals == nil && computedNameContains == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// getComputedAttributes will need logging parameters
|
||||
let computedAttrs = getComputedAttributes(
|
||||
for: element,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
if let currentComputedNameAny = computedAttrs[computedNameAttributeKey]?.value,
|
||||
// Assuming .value is how you get it from the AttributeData struct
|
||||
let currentComputedName = currentComputedNameAny as? String {
|
||||
if let equals = computedNameEquals {
|
||||
if currentComputedName != equals {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' != '\(equals)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if let contains = computedNameContains {
|
||||
if !currentComputedName.localizedCaseInsensitiveContains(contains) {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' does not contain '\(contains)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,181 @@
|
||||
// ElementSearch.swift - Contains search and element collection logic
|
||||
|
||||
import ApplicationServices
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
// Variable DEBUG_LOGGING_ENABLED is expected to be globally available from Logging.swift
|
||||
// Element is now the primary type for UI elements.
|
||||
// MARK: - Environment Variable & Global Constants
|
||||
|
||||
// decodeExpectedArray MOVED to Utils/GeneralParsingUtils.swift
|
||||
private func getEnvVar(_ name: String) -> String? {
|
||||
guard let value = getenv(name) else { return nil }
|
||||
return String(cString: value)
|
||||
}
|
||||
|
||||
private let AXORC_JSON_LOG_ENABLED: Bool = {
|
||||
let envValue = getEnvVar("AXORC_JSON_LOG")?.lowercased()
|
||||
fputs("[ElementSearch.swift] AXORC_JSON_LOG env var value: \(envValue ?? "not set") -> JSON logging: \(envValue == "true")\n", stderr)
|
||||
return envValue == "true"
|
||||
}()
|
||||
|
||||
// PathHintComponent and criteriaMatch are now in SearchCriteriaUtils.swift
|
||||
|
||||
// MARK: - Main Search Logic (findElementViaPathAndCriteria and its helpers)
|
||||
@MainActor
|
||||
func findElementViaPathAndCriteria(
|
||||
application: Element,
|
||||
locator: Locator,
|
||||
maxDepth: Int?,
|
||||
isDebugLoggingEnabledParam: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Element? {
|
||||
var tempNilLogs: [String] = []
|
||||
|
||||
// ADDED DEBUG LOGGING
|
||||
if isDebugLoggingEnabledParam {
|
||||
let pathHintDebug = locator.root_element_path_hint?.joined(separator: " -> ") ?? "nil"
|
||||
let initialMessage = "[findElementViaPathAndCriteria ENTRY] locator.criteria: \(locator.criteria), locator.root_element_path_hint: \(pathHintDebug)"
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(initialMessage, applicationName: application.pid(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).map { String($0) }, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
// END ADDED DEBUG LOGGING
|
||||
|
||||
func dLog(_ message: String, depth: Int? = nil, status: String? = nil, element: Element? = nil, c: [String: String]? = nil, md: Int? = nil) {
|
||||
if !AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabledParam {
|
||||
var logMessage = message
|
||||
if let depth_ = depth, let status_ = status, let element_ = element {
|
||||
let role = element_.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs) ?? "nil"
|
||||
let title = element_.title(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)?.truncated(to: 30) ?? "nil"
|
||||
let id = element_.identifier(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)?.truncated(to: 30) ?? "nil"
|
||||
let criteriaDesc = c?.description.truncated(to: 50) ?? locator.criteria.description.truncated(to: 50)
|
||||
let maxDepthDesc = md ?? maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
|
||||
logMessage = "search [D\(depth_)]: Path:\(element_.generatePathArray(upTo: application, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).suffix(3).joined(separator: "/")), Status:\(status_), Elem:\(role) T:'\(title)' ID:'\(id)', Crit:\(criteriaDesc), MaxD:\(maxDepthDesc)"
|
||||
}
|
||||
let appPidString = application.pid(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs).map { String($0) }
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(logMessage, applicationName: appPidString, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
func writeSearchLogEntry(depth: Int, element: Element?, criteriaForEntry: [String: String]?, maxDepthForEntry: Int, status: String, isMatch: Bool?) {
|
||||
if AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabledParam {
|
||||
let role: String? = element?.role(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
|
||||
let title: String? = element?.title(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
|
||||
let identifier: String? = element?.identifier(isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)
|
||||
|
||||
let entry = SearchLogEntry(
|
||||
d: depth,
|
||||
eR: role?.truncatedToMaxLogAbbrev(),
|
||||
eT: title?.truncatedToMaxLogAbbrev(),
|
||||
eI: identifier?.truncatedToMaxLogAbbrev(),
|
||||
mD: maxDepthForEntry,
|
||||
c: criteriaForEntry?.mapValues { $0.truncatedToMaxLogAbbrev() } ?? locator.criteria.mapValues { $0.truncatedToMaxLogAbbrev() },
|
||||
s: status,
|
||||
iM: isMatch
|
||||
)
|
||||
if let jsonData = try? JSONEncoder().encode(entry), let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
fputs("\(jsonString)\n", stderr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func navigateToElementByPathHint(pathHint: [PathHintComponent], initialSearchElement: Element, pathHintMaxDepth: Int) -> Element? {
|
||||
var currentElementInPath = initialSearchElement
|
||||
dLog("PathHintNav: Starting with \(pathHint.count) components from \(initialSearchElement.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs))")
|
||||
|
||||
for (index, pathComponent) in pathHint.enumerated() {
|
||||
let currentNavigationDepth = index
|
||||
dLog("PathHintNav: Visiting comp #\(index)", depth: currentNavigationDepth, status: "pathVis", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
|
||||
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathVis", isMatch: nil)
|
||||
|
||||
if !pathComponent.matches(element: currentElementInPath, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog("PathHintNav: No match for comp #\(index)", depth: currentNavigationDepth, status: "pathNoMatch", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
|
||||
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathNoMatch", isMatch: false)
|
||||
return nil
|
||||
}
|
||||
|
||||
dLog("PathHintNav: Matched comp #\(index)", depth: currentNavigationDepth, status: "pathMatch", element: currentElementInPath, c: pathComponent.criteria, md: pathHintMaxDepth)
|
||||
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: pathComponent.criteria, maxDepthForEntry: pathHintMaxDepth, status: "pathMatch", isMatch: true)
|
||||
|
||||
if index == pathHint.count - 1 {
|
||||
return currentElementInPath
|
||||
}
|
||||
|
||||
let nextPathComponentCriteria = pathHint[index + 1].criteria
|
||||
var foundNextChild: Element?
|
||||
if let children = currentElementInPath.children(isDebugLoggingEnabled: isDebugLoggingEnabledParam, currentDebugLogs: ¤tDebugLogs) {
|
||||
for child in children {
|
||||
let tempPathComponent = PathHintComponent(criteria: nextPathComponentCriteria)
|
||||
if tempPathComponent.matches(element: child, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) {
|
||||
currentElementInPath = child
|
||||
foundNextChild = child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundNextChild == nil {
|
||||
dLog("PathHintNav: Could not find child for next comp #\(index + 1)", depth: currentNavigationDepth, status: "pathChildFail", element: currentElementInPath, c: nextPathComponentCriteria, md: pathHintMaxDepth)
|
||||
writeSearchLogEntry(depth: currentNavigationDepth, element: currentElementInPath, criteriaForEntry: nextPathComponentCriteria, maxDepthForEntry: pathHintMaxDepth, status: "pathChildFail", isMatch: false)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return currentElementInPath
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func traverseAndSearch(currentElement: Element, currentDepth: Int, effectiveMaxDepth: Int) -> Element? {
|
||||
dLog("Traverse: Visiting", depth: currentDepth, status: "vis", element: currentElement, md: effectiveMaxDepth)
|
||||
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "vis", isMatch: nil)
|
||||
|
||||
if criteriaMatch(element: currentElement, criteria: locator.criteria, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog("Traverse: Found", depth: currentDepth, status: "found", element: currentElement, md: effectiveMaxDepth)
|
||||
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "found", isMatch: true)
|
||||
return currentElement
|
||||
} else {
|
||||
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "noMatch", isMatch: false)
|
||||
}
|
||||
|
||||
if currentDepth >= effectiveMaxDepth {
|
||||
dLog("Traverse: MaxDepth", depth: currentDepth, status: "maxD", element: currentElement, md: effectiveMaxDepth)
|
||||
writeSearchLogEntry(depth: currentDepth, element: currentElement, criteriaForEntry: locator.criteria, maxDepthForEntry: effectiveMaxDepth, status: "maxD", isMatch: false)
|
||||
return nil
|
||||
}
|
||||
|
||||
if let children = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabledParam, currentDebugLogs: ¤tDebugLogs) {
|
||||
for child in children {
|
||||
if let found = traverseAndSearch(currentElement: child, currentDepth: currentDepth + 1, effectiveMaxDepth: effectiveMaxDepth) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var searchStartElement = application
|
||||
let resolvedMaxDepth = maxDepth ?? AXMiscConstants.defaultMaxDepthSearch
|
||||
|
||||
if let pathHintStrings = locator.root_element_path_hint, !pathHintStrings.isEmpty {
|
||||
let pathHintComponents = pathHintStrings.compactMap { PathHintComponent(pathSegment: $0, isDebugLoggingEnabled: isDebugLoggingEnabledParam, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) }
|
||||
if !pathHintComponents.isEmpty && pathHintComponents.count == pathHintStrings.count {
|
||||
dLog("Starting path hint navigation. Number of components: \(pathHintComponents.count)")
|
||||
if let elementFromPathHint = navigateToElementByPathHint(pathHint: pathHintComponents, initialSearchElement: application, pathHintMaxDepth: pathHintComponents.count - 1) {
|
||||
dLog("Path hint navigation successful. New start: \(elementFromPathHint.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). Starting criteria search.")
|
||||
searchStartElement = elementFromPathHint
|
||||
} else {
|
||||
dLog("Path hint navigation failed. Full search from app root.")
|
||||
}
|
||||
} else {
|
||||
dLog("Path hint strings provided but failed to parse into components or some were invalid. Full search from app root.")
|
||||
}
|
||||
} else {
|
||||
dLog("No path hint provided. Searching from application root.")
|
||||
}
|
||||
|
||||
return traverseAndSearch(currentElement: searchStartElement, currentDepth: 0, effectiveMaxDepth: resolvedMaxDepth)
|
||||
}
|
||||
|
||||
enum ElementMatchStatus {
|
||||
case fullMatch // Role, attributes, and (if specified) action all match
|
||||
case partialMatch_actionMissing // Role and attributes match, but a required action is missing
|
||||
case noMatch // Role or attributes do not match
|
||||
case fullMatch
|
||||
case partialMatch_actionMissing
|
||||
case noMatch
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -24,71 +187,33 @@ internal func evaluateElementAgainstCriteria(
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> ElementMatchStatus {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
func el_dLog(_ message: String) {
|
||||
if !AXORC_JSON_LOG_ENABLED && isDebugLoggingEnabled { currentDebugLogs.append(message) }
|
||||
}
|
||||
var tempLogs: [String] = []
|
||||
|
||||
var tempLogs: [String] = [] // For calls to Element methods that need their own log scope temporarily
|
||||
let currentElementRoleForLog: String? = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
|
||||
let wantedRoleFromCriteria = locator.criteria[AXAttributeNames.kAXRoleAttribute]
|
||||
|
||||
let currentElementRoleForLog: String? = element.role(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
let wantedRoleFromCriteria = locator.criteria[kAXRoleAttribute]
|
||||
var roleMatchesCriteria = false
|
||||
|
||||
if let currentRole = currentElementRoleForLog, let roleToMatch = wantedRoleFromCriteria, !roleToMatch.isEmpty,
|
||||
roleToMatch != "*" {
|
||||
if let currentRole = currentElementRoleForLog, let roleToMatch = wantedRoleFromCriteria, !roleToMatch.isEmpty, roleToMatch != "*" {
|
||||
roleMatchesCriteria = (currentRole == roleToMatch)
|
||||
} else {
|
||||
roleMatchesCriteria = true // Wildcard/empty/nil role in criteria is a match
|
||||
let wantedRoleStr = wantedRoleFromCriteria ?? "any"
|
||||
let currentRoleStr = currentElementRoleForLog ?? "nil"
|
||||
dLog(
|
||||
"evaluateElementAgainstCriteria [D\(depth)]: Wildcard/empty/nil role in criteria ('\(wantedRoleStr)') considered a match for element role \(currentRoleStr)."
|
||||
)
|
||||
roleMatchesCriteria = true
|
||||
}
|
||||
|
||||
if !roleMatchesCriteria {
|
||||
dLog(
|
||||
"evaluateElementAgainstCriteria [D\(depth)]: Role mismatch. Element role: \(currentElementRoleForLog ?? "nil"), Expected: \(wantedRoleFromCriteria ?? "any"). No match."
|
||||
)
|
||||
if !roleMatchesCriteria { return .noMatch }
|
||||
|
||||
if !criteriaMatch(element: element, criteria: locator.criteria, isDebugLoggingEnabled: isDebugLoggingEnabled, axorcJsonLogEnabled: AXORC_JSON_LOG_ENABLED, currentDebugLogs: ¤tDebugLogs) {
|
||||
return .noMatch
|
||||
}
|
||||
|
||||
// Role matches, now check other attributes
|
||||
// attributesMatch will also need isDebugLoggingEnabled, currentDebugLogs
|
||||
if !attributesMatch(
|
||||
element: element,
|
||||
matchDetails: locator.criteria,
|
||||
depth: depth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) {
|
||||
// attributesMatch itself will log the specific mismatch reason
|
||||
dLog("evaluateElementAgainstCriteria [D\(depth)]: attributesMatch returned false. No match.")
|
||||
return .noMatch
|
||||
}
|
||||
|
||||
// Role and attributes match. Now check for required action.
|
||||
if let requiredAction = actionToVerify, !requiredAction.isEmpty {
|
||||
if !element.isActionSupported(
|
||||
requiredAction,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
) {
|
||||
dLog(
|
||||
"evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched, but required action '\(requiredAction)' is MISSING."
|
||||
)
|
||||
let actionRequirement = actionToVerify ?? locator.requireAction
|
||||
if let requiredAction = actionRequirement, !requiredAction.isEmpty {
|
||||
if !element.isActionSupported(requiredAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
|
||||
return .partialMatch_actionMissing
|
||||
}
|
||||
dLog(
|
||||
"evaluateElementAgainstCriteria [D\(depth)]: Role, Attributes, and Required Action '\(requiredAction)' all MATCH."
|
||||
)
|
||||
} else {
|
||||
dLog(
|
||||
"evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched. No action to verify or action already included in locator.criteria for attributesMatch."
|
||||
)
|
||||
}
|
||||
|
||||
return .fullMatch
|
||||
}
|
||||
|
||||
@ -97,113 +222,34 @@ public func search(element: Element,
|
||||
locator: Locator,
|
||||
requireAction: String?,
|
||||
depth: Int = 0,
|
||||
maxDepth: Int = DEFAULT_MAX_DEPTH_SEARCH,
|
||||
isDebugLoggingEnabled: Bool
|
||||
/* REMOVED: currentDebugLogs: inout [String] */) -> (foundElement: Element?, logs: [String]) { // CHANGED RETURN TYPE
|
||||
fputs("SEARCH_FUNCTION_RAW_PRINT_STDERR: Depth \(depth), isDebug: \(isDebugLoggingEnabled)\n", stderr)
|
||||
var internalSearchLogs: [String] = [] // NEW: Internal log storage
|
||||
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]) -> Element? {
|
||||
var tempLogs: [String] = []
|
||||
if depth > maxDepth { return nil }
|
||||
|
||||
// DIRECT APPEND AND LOCAL LET FOR DEBUG FLAG
|
||||
internalSearchLogs.append("SEARCH_ENTRY_DIRECT_APPEND: Depth \(depth), isDebugLoggingEnabledParam: \(isDebugLoggingEnabled)")
|
||||
let localDebugEnabled = isDebugLoggingEnabled
|
||||
internalSearchLogs.append("SEARCH_ENTRY_LOCALDEBUG: localDebugEnabled is \(localDebugEnabled)")
|
||||
|
||||
func dLog(_ message: String) { if localDebugEnabled { internalSearchLogs.append(message) } } // Appends to internalSearchLogs
|
||||
|
||||
var tempLogsForElementMethods: [String] = [] // For calls to Element methods that require inout logs
|
||||
|
||||
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
|
||||
// Calls to element.role, element.title etc. will use tempLogsForElementMethods
|
||||
// Their logs aren't the primary concern for *this* refactor's test, but they need a valid inout array.
|
||||
let roleStr = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForElementMethods) ?? "nil"
|
||||
let titleStr = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogsForElementMethods) ?? "N/A"
|
||||
internalSearchLogs.append(contentsOf: tempLogsForElementMethods) // Append logs from element methods
|
||||
tempLogsForElementMethods.removeAll() // Clear for next use
|
||||
|
||||
dLog(
|
||||
"search [D\(depth)]: Visiting. Role: \(roleStr), Title: \(titleStr). Locator Criteria: [\(criteriaDesc)], Action: \(requireAction ?? "none")"
|
||||
)
|
||||
|
||||
if depth > maxDepth {
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForElementMethods
|
||||
)
|
||||
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
|
||||
tempLogsForElementMethods.removeAll()
|
||||
dLog("search [D\(depth)]: Max depth \(maxDepth) reached for element \(briefDesc).")
|
||||
return (nil, internalSearchLogs) // RETURN TUPLE
|
||||
}
|
||||
|
||||
// evaluateElementAgainstCriteria still uses inout, its logs will be added to internalSearchLogs
|
||||
var logsFromEvaluate: [String] = []
|
||||
let matchStatus = evaluateElementAgainstCriteria(element: element,
|
||||
locator: locator,
|
||||
actionToVerify: requireAction,
|
||||
actionToVerify: requireAction ?? locator.requireAction,
|
||||
depth: depth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &logsFromEvaluate)
|
||||
internalSearchLogs.append(contentsOf: logsFromEvaluate)
|
||||
currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
if matchStatus == .fullMatch {
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForElementMethods
|
||||
)
|
||||
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
|
||||
tempLogsForElementMethods.removeAll()
|
||||
dLog(
|
||||
"search [D\(depth)]: evaluateElementAgainstCriteria returned .fullMatch for \(briefDesc). Returning element."
|
||||
)
|
||||
return (element, internalSearchLogs) // RETURN TUPLE
|
||||
}
|
||||
if matchStatus == .fullMatch { return element }
|
||||
|
||||
let briefDesc = element.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForElementMethods
|
||||
)
|
||||
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
|
||||
tempLogsForElementMethods.removeAll()
|
||||
|
||||
if matchStatus == .partialMatch_actionMissing {
|
||||
dLog(
|
||||
"search [D\(depth)]: Element \(briefDesc) matched criteria but missed action '\(requireAction ?? "")'. Continuing child search."
|
||||
)
|
||||
}
|
||||
if matchStatus == .noMatch {
|
||||
dLog("search [D\(depth)]: Element \(briefDesc) did not match criteria. Continuing child search.")
|
||||
}
|
||||
|
||||
let childrenToSearch: [Element] = element.children(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogsForElementMethods // Pass tempLogsForElementMethods here
|
||||
) ?? []
|
||||
internalSearchLogs.append(contentsOf: tempLogsForElementMethods)
|
||||
tempLogsForElementMethods.removeAll()
|
||||
|
||||
|
||||
if !childrenToSearch.isEmpty {
|
||||
for childElement in childrenToSearch {
|
||||
// RECURSIVE CALL
|
||||
let recursiveResult = search( // No longer passes currentDebugLogs
|
||||
element: childElement,
|
||||
locator: locator,
|
||||
requireAction: requireAction,
|
||||
depth: depth + 1,
|
||||
maxDepth: maxDepth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled
|
||||
// Removed: currentDebugLogs: ¤tDebugLogs -> now &internalSearchLogs
|
||||
)
|
||||
internalSearchLogs.append(contentsOf: recursiveResult.logs) // Append logs from recursive call
|
||||
if let found = recursiveResult.foundElement {
|
||||
return (found, internalSearchLogs) // RETURN TUPLE
|
||||
}
|
||||
let childrenToSearch: [Element] = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
|
||||
for childElement in childrenToSearch {
|
||||
if let found = search(element: childElement,
|
||||
locator: locator,
|
||||
requireAction: requireAction,
|
||||
depth: depth + 1,
|
||||
maxDepth: maxDepth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return (nil, internalSearchLogs) // RETURN TUPLE
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -218,79 +264,36 @@ public func collectAll(
|
||||
elementsBeingProcessed: inout Set<Element>,
|
||||
foundElements: inout [Element],
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String] // Added logging parameter
|
||||
currentDebugLogs: inout [String]
|
||||
) {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For calls to Element methods
|
||||
|
||||
let briefDescCurrent = currentElement.briefDescription(
|
||||
option: .default,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
|
||||
if elementsBeingProcessed.contains(currentElement) || currentPath.contains(currentElement) {
|
||||
dLog("collectAll [D\(depth)]: Cycle detected or element \(briefDescCurrent) already processed/in path.")
|
||||
return
|
||||
}
|
||||
var tempLogs: [String] = []
|
||||
if elementsBeingProcessed.contains(currentElement) || currentPath.contains(currentElement) { return }
|
||||
elementsBeingProcessed.insert(currentElement)
|
||||
|
||||
if foundElements.count >= maxElements {
|
||||
dLog(
|
||||
"collectAll [D\(depth)]: Max elements limit of \(maxElements) reached before processing \(briefDescCurrent)."
|
||||
)
|
||||
if foundElements.count >= maxElements || depth > maxDepth {
|
||||
elementsBeingProcessed.remove(currentElement)
|
||||
return
|
||||
}
|
||||
if depth > maxDepth {
|
||||
dLog("collectAll [D\(depth)]: Max depth \(maxDepth) reached for \(briefDescCurrent).")
|
||||
elementsBeingProcessed.remove(currentElement)
|
||||
return
|
||||
}
|
||||
|
||||
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
|
||||
dLog(
|
||||
"collectAll [D\(depth)]: Visiting \(briefDescCurrent). Criteria: [\(criteriaDesc)], Action: \(locator.requireAction ?? "none")"
|
||||
)
|
||||
|
||||
let matchStatus = evaluateElementAgainstCriteria(element: currentElement,
|
||||
locator: locator,
|
||||
actionToVerify: locator.requireAction,
|
||||
depth: depth,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs) // Pass through logs
|
||||
currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
if matchStatus == .fullMatch {
|
||||
if foundElements.count < maxElements {
|
||||
if !foundElements.contains(currentElement) {
|
||||
foundElements.append(currentElement)
|
||||
dLog("collectAll [D\(depth)]: Added \(briefDescCurrent). Hits: \(foundElements.count)/\(maxElements)")
|
||||
} else {
|
||||
dLog(
|
||||
"collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but already in foundElements."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
dLog(
|
||||
"collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but maxElements (\(maxElements)) already reached."
|
||||
)
|
||||
if !foundElements.contains(currentElement) {
|
||||
foundElements.append(currentElement)
|
||||
}
|
||||
}
|
||||
|
||||
let childrenToExplore: [Element] = currentElement.children(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
) ?? []
|
||||
let childrenToExplore: [Element] = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
|
||||
elementsBeingProcessed.remove(currentElement)
|
||||
|
||||
let newPath = currentPath + [currentElement]
|
||||
for child in childrenToExplore {
|
||||
if foundElements.count >= maxElements {
|
||||
dLog(
|
||||
"collectAll [D\(depth)]: Max elements (\(maxElements)) reached during child traversal of \(briefDescCurrent). Stopping further exploration for this branch."
|
||||
)
|
||||
break
|
||||
}
|
||||
if foundElements.count >= maxElements { break }
|
||||
collectAll(
|
||||
appElement: appElement,
|
||||
locator: locator,
|
||||
@ -302,7 +305,16 @@ public func collectAll(
|
||||
elementsBeingProcessed: &elementsBeingProcessed,
|
||||
foundElements: &foundElements,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs // Pass through logs
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Notes for compilation:
|
||||
// 1. ValueUnwrapper.unwrap should be available.
|
||||
// 2. AXorcist.formatDebugLogMessage should be available.
|
||||
// 3. Element struct and its methods must be correctly defined.
|
||||
// 4. Locator struct must be defined with `criteria: [String: String]`, `root_element_path_hint: [String]?`, and `requireAction: String?`.
|
||||
// 5. AXAttributeNames.kAXRoleAttribute should be a defined constant (String).
|
||||
// 6. ValueFormatOption enum (with .default, .short cases) must be available for Element.briefDescription.
|
||||
// 7. SearchLogEntry struct is now in Models.swift
|
||||
|
||||
147
Sources/AXorcist/Search/PathNavigator.swift
Normal file
147
Sources/AXorcist/Search/PathNavigator.swift
Normal file
@ -0,0 +1,147 @@
|
||||
// PathNavigator.swift - Contains logic for navigating element hierarchies using path hints
|
||||
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Note: Assumes Element, PathUtils, Attribute, AXorcist.formatDebugLogMessage are available.
|
||||
|
||||
// Helper to check if the current element matches a specific attribute-value pair
|
||||
@MainActor
|
||||
internal func currentElementMatchesPathComponent(
|
||||
_ element: Element,
|
||||
attributeName: String,
|
||||
expectedValue: String,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String] // For logging
|
||||
) -> Bool {
|
||||
if attributeName.isEmpty { // Should not happen if parsePathComponent is robust
|
||||
return false
|
||||
}
|
||||
// Assuming Element.attribute can handle logging appropriately based on isDebugLoggingEnabled and AXORC_JSON_LOG_ENABLED
|
||||
if let actualValue = element.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
if actualValue == expectedValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Updated navigateToElement to prioritize children
|
||||
@MainActor
|
||||
internal func navigateToElement(
|
||||
from startElement: Element,
|
||||
pathHint: [String],
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Element? {
|
||||
func dLog(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
// Use the passed-in isDebugLoggingEnabled
|
||||
if isDebugLoggingEnabled {
|
||||
// Assumes AXorcist.formatDebugLogMessage is accessible, might need to be public or this moved to an AXorcist extension.
|
||||
// For now, let it be, build will tell if it's an issue.
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: file, function: function, line: line))
|
||||
}
|
||||
}
|
||||
|
||||
var currentElement = startElement
|
||||
var currentPathSegmentForLog = ""
|
||||
|
||||
for (index, pathComponentString) in pathHint.enumerated() {
|
||||
currentPathSegmentForLog += (index > 0 ? " -> " : "") + pathComponentString
|
||||
// Element.briefDescription needs access to logging parameters
|
||||
let briefDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
dLog("Navigating: Processing path component '\(pathComponentString)' from current element: \(briefDesc)")
|
||||
|
||||
let (attributeName, expectedValue) = PathUtils.parsePathComponent(pathComponentString)
|
||||
guard !attributeName.isEmpty else {
|
||||
dLog("CRITICAL_NAV_PARSE_FAILURE_MARKER: Empty attribute name from pathComponentString '\(pathComponentString)'")
|
||||
return nil
|
||||
}
|
||||
|
||||
var foundMatchForThisComponent = false
|
||||
var newElementForNextStep: Element?
|
||||
|
||||
// Priority 1: Check children using Element.children()
|
||||
if let childrenFromElementDotChildren = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog("Child count from Element.children(): \(childrenFromElementDotChildren.count)")
|
||||
for child in childrenFromElementDotChildren {
|
||||
let childBriefDescForLog = child.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
if let actualValue = child.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog(" [Nav Child Check 1] Child: \(childBriefDescForLog), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
|
||||
if actualValue == expectedValue {
|
||||
dLog("Matched child (from Element.children): \(childBriefDescForLog) for '\(attributeName):\(expectedValue)'")
|
||||
newElementForNextStep = child
|
||||
foundMatchForThisComponent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dLog("Current element \(briefDesc) has no children from Element.children() or children array was nil.")
|
||||
}
|
||||
|
||||
// FALLBACK: If no child matched via Element.children(), try direct AXAttributeNames.kAXChildrenAttribute call (Heisenbug workaround)
|
||||
if !foundMatchForThisComponent {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: No match from Element.children(). Trying direct AXAttributeNames.kAXChildrenAttribute fallback.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
|
||||
var directChildrenValue: CFTypeRef?
|
||||
let directChildrenError = AXUIElementCopyAttributeValue(currentElement.underlyingElement, AXAttributeNames.kAXChildrenAttribute as CFString, &directChildrenValue)
|
||||
|
||||
let currentElementDescForFallbackLog = isDebugLoggingEnabled ? currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) : "Element(debug_off)"
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Fallback is for element: \(currentElementDescForFallbackLog)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
|
||||
if directChildrenError == .success, let cfArray = directChildrenValue, CFGetTypeID(cfArray) == CFArrayGetTypeID() {
|
||||
if let directAxElements = cfArray as? [AXUIElement] {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback found \(directAxElements.count) raw children for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
for axChild in directAxElements {
|
||||
let childElement = Element(axChild)
|
||||
let childBriefDescForLogFallback = childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
if let actualValue = childElement.attribute(Attribute<String>(attributeName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
dLog(" [Nav Child Check 2-Fallback] Child: \(childBriefDescForLogFallback), Attribute '\(attributeName)': [\(actualValue)] (Expected: [\(expectedValue)])")
|
||||
if actualValue == expectedValue {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Matched child (from direct fallback) for '\(attributeName):\(expectedValue)' on \(currentElementDescForFallbackLog). Child: \(childElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
newElementForNextStep = childElement
|
||||
foundMatchForThisComponent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: CFArray failed to cast to [AXUIElement] for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
} else if directChildrenError != .success {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: Error fetching for \(currentElementDescForFallbackLog): \(directChildrenError.rawValue)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
} else {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("navigateToElement: Direct AXAttributeNames.kAXChildrenAttribute fallback: No children or not an array for \(currentElementDescForFallbackLog).", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: If no child matched (even after fallback), check current element itself
|
||||
if !foundMatchForThisComponent {
|
||||
// Pass currentDebugLogs by reference to the global currentElementMatchesPathComponent
|
||||
let matchResult = currentElementMatchesPathComponent(
|
||||
currentElement,
|
||||
attributeName: attributeName,
|
||||
expectedValue: expectedValue,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs // Pass by ref
|
||||
)
|
||||
|
||||
if matchResult {
|
||||
dLog("Current element \(briefDesc) itself matches '\(attributeName):\(expectedValue)'. Retaining current element for this step.")
|
||||
newElementForNextStep = currentElement
|
||||
foundMatchForThisComponent = true
|
||||
}
|
||||
}
|
||||
|
||||
if foundMatchForThisComponent, let nextElement = newElementForNextStep {
|
||||
currentElement = nextElement
|
||||
} else {
|
||||
dLog("Neither current element \(briefDesc) nor its children (after all checks) matched '\(attributeName):\(expectedValue)'. Path: \(currentPathSegmentForLog) // CHILD_MATCH_FAILURE_MARKER")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dLog("Navigation successful. Final element: \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs))")
|
||||
return currentElement
|
||||
}
|
||||
89
Sources/AXorcist/Search/SearchCriteriaUtils.swift
Normal file
89
Sources/AXorcist/Search/SearchCriteriaUtils.swift
Normal file
@ -0,0 +1,89 @@
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Note: This file assumes AXAttributeNames.kAXRoleAttribute is available from AXAttributeNameConstants.swift
|
||||
// and ValueUnwrapper is available from its respective file.
|
||||
|
||||
// MARK: - PathHintComponent Definition
|
||||
@MainActor
|
||||
struct PathHintComponent {
|
||||
let criteria: [String: String]
|
||||
|
||||
init?(pathSegment: String, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) {
|
||||
var parsedCriteria: [String: String] = [:]
|
||||
let pairs = pathSegment.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
for pair in pairs {
|
||||
let keyValue = pair.split(separator: "=", maxSplits: 1).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
if keyValue.count == 2 {
|
||||
parsedCriteria[String(keyValue[0])] = String(keyValue[1])
|
||||
} else {
|
||||
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent: Invalid key-value pair: \(pair)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
}
|
||||
if parsedCriteria.isEmpty && !pathSegment.isEmpty {
|
||||
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent: Path segment \"\(pathSegment)\" parsed into empty criteria.", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
self.criteria = parsedCriteria
|
||||
if isDebugLoggingEnabled && !axorcJsonLogEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage("PathHintComponent initialized with criteria: \(self.criteria) from segment: \(pathSegment)", applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience initializer if criteria is already a dictionary
|
||||
init(criteria: [String: String]) {
|
||||
self.criteria = criteria
|
||||
}
|
||||
|
||||
func matches(element: Element, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
|
||||
// Pass axorcJsonLogEnabled to criteriaMatch
|
||||
return criteriaMatch(element: element, criteria: self.criteria, isDebugLoggingEnabled: isDebugLoggingEnabled, axorcJsonLogEnabled: axorcJsonLogEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Criteria Matching Helper
|
||||
@MainActor
|
||||
func criteriaMatch(element: Element, criteria: [String: String]?, isDebugLoggingEnabled: Bool, axorcJsonLogEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
|
||||
guard let criteria = criteria, !criteria.isEmpty else {
|
||||
return true // No criteria means an automatic match
|
||||
}
|
||||
|
||||
func cLog(_ message: String) {
|
||||
// Use the passed-in axorcJsonLogEnabled parameter
|
||||
if !axorcJsonLogEnabled && isDebugLoggingEnabled {
|
||||
currentDebugLogs.append(AXorcist.formatDebugLogMessage(message, applicationName: nil, commandID: nil, file: #file, function: #function, line: #line))
|
||||
}
|
||||
}
|
||||
var tempNilLogs: [String] = [] // For briefDescription calls that don't need to pollute main logs
|
||||
|
||||
for (key, expectedValue) in criteria {
|
||||
// Handle wildcard for role if specified
|
||||
if key == AXAttributeNames.kAXRoleAttribute && expectedValue == "*" { continue }
|
||||
|
||||
var attributeValueCFType: CFTypeRef?
|
||||
// Directly use underlyingElement for AX API calls
|
||||
let error = AXUIElementCopyAttributeValue(element.underlyingElement, key as CFString, &attributeValueCFType)
|
||||
|
||||
guard error == .success, let actualValueCF = attributeValueCFType else {
|
||||
cLog("Attribute \(key) not found or error \(error.rawValue) on element \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). No match.")
|
||||
return false
|
||||
}
|
||||
|
||||
// Use ValueUnwrapper to convert CFTypeRef to a Swift type
|
||||
// Assuming ValueUnwrapper.unwrap is available and correctly handles logging parameters
|
||||
let actualValueSwift: Any? = ValueUnwrapper.unwrap(actualValueCF, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
let actualValueString = String(describing: actualValueSwift ?? "nil_after_unwrap")
|
||||
|
||||
// Perform case-insensitive comparison or exact match
|
||||
if !(actualValueString.localizedCaseInsensitiveContains(expectedValue) || actualValueString == expectedValue) {
|
||||
cLog("Attribute '\(key)' mismatch: Expected '\(expectedValue)', Got '\(actualValueString)'. Element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)). No match.")
|
||||
return false
|
||||
}
|
||||
cLog("Attribute '\(key)' matched: Expected '\(expectedValue)', Got '\(actualValueString)'.")
|
||||
}
|
||||
cLog("All criteria matched for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: false, currentDebugLogs: &tempNilLogs)).")
|
||||
return true
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
// PathUtils.swift - Utilities for parsing paths and navigating element hierarchies.
|
||||
|
||||
import ApplicationServices // For Element, AXUIElement and kAX...Attribute constants
|
||||
import ApplicationServices // For Element, AXUIElement and AX...Attribute constants
|
||||
import Foundation
|
||||
|
||||
// Assumes Element is defined (likely via AXSwift an extension or typealias)
|
||||
// debug() is assumed to be globally available from Logging.swift
|
||||
// axValue<T>() is assumed to be globally available from ValueHelpers.swift
|
||||
// kAXWindowRole, kAXWindowsAttribute, kAXChildrenAttribute, kAXRoleAttribute from AccessibilityConstants.swift
|
||||
// AXRoleNames.kAXWindowRole, AXAttributeNames.kAXWindowsAttribute, AXAttributeNames.kAXChildrenAttribute, AXAttributeNames.kAXRoleAttribute from namespaced constants
|
||||
|
||||
237
Sources/AXorcist/Search/SpecificAttributeMatchers.swift
Normal file
237
Sources/AXorcist/Search/SpecificAttributeMatchers.swift
Normal file
@ -0,0 +1,237 @@
|
||||
// SpecificAttributeMatchers.swift - Contains specific helper functions for attribute matching.
|
||||
|
||||
import ApplicationServices
|
||||
import Foundation
|
||||
|
||||
// Assumes Element, Attribute, AXMiscConstants.computedNameAttributeKey, AXMiscConstants.kAXNotAvailableString,
|
||||
// AXAttributeNames.kAXRoleAttribute, AXAttributeNames.kAXEnabledAttribute, AXAttributeNames.kAXFocusedAttribute, AXAttributeNames.kAXHiddenAttribute,
|
||||
// AXAttributeNames.kAXElementBusyAttribute, AXMiscConstants.isIgnoredAttributeKey, AXAttributeNames.kAXMainAttribute,
|
||||
// AXAttributeNames.kAXActionNamesAttribute, AXAttributeNames.kAXAllowedValuesAttribute, AXAttributeNames.kAXChildrenAttribute are available.
|
||||
// Assumes decodeExpectedArray (from ValueParser or similar) and getComputedAttributes (from AttributeHelpers) are available.
|
||||
|
||||
@MainActor
|
||||
internal func matchStringAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
if let currentValue = element.attribute(
|
||||
Attribute<String>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
) {
|
||||
if currentValue != expectedValueString {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' expected '\(expectedValueString)', but found '\(currentValue)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if expectedValueString
|
||||
.lowercased() == "nil" || expectedValueString == AXMiscConstants.kAXNotAvailableString || expectedValueString.isEmpty {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' not found, but expected value ('\(expectedValueString)') suggests absence is OK. Match for this key."
|
||||
)
|
||||
return true
|
||||
} else {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Attribute '\(key)' (expected '\(expectedValueString)') not found or not convertible to String. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchArrayAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
guard let expectedArray = decodeExpectedArray(
|
||||
fromString: expectedValueString,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
) else {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
var actualArray: [String]?
|
||||
if key == AXAttributeNames.kAXActionNamesAttribute {
|
||||
actualArray = element.supportedActions(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
} else if key == AXAttributeNames.kAXAllowedValuesAttribute {
|
||||
actualArray = element.attribute(
|
||||
Attribute<[String]>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
} else if key == AXAttributeNames.kAXChildrenAttribute {
|
||||
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?
|
||||
.map { childElement -> String in
|
||||
var childLogs: [String] = []
|
||||
return childElement
|
||||
.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
|
||||
}
|
||||
} else {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Unknown array key '\(key)'. This function needs to be extended for this key."
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if let actual = actualArray {
|
||||
if Set(actual) != Set(expectedArray) {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' expected '\(expectedArray)', but found '\(actual)'. Sets differ. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if expectedArray.isEmpty {
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' not found, but expected array was empty. Match for this key."
|
||||
)
|
||||
return true
|
||||
}
|
||||
dLog(
|
||||
"matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchBooleanAttribute(
|
||||
element: Element,
|
||||
key: String,
|
||||
expectedValueString: String,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
var currentBoolValue: Bool?
|
||||
|
||||
switch key {
|
||||
case AXAttributeNames.kAXEnabledAttribute: currentBoolValue = element.isEnabled(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case AXAttributeNames.kAXFocusedAttribute: currentBoolValue = element.isFocused(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case AXAttributeNames.kAXHiddenAttribute: currentBoolValue = element.isHidden(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case AXAttributeNames.kAXElementBusyAttribute: currentBoolValue = element.isElementBusy(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case AXMiscConstants.isIgnoredAttributeKey: currentBoolValue = element.isIgnored(
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
case AXAttributeNames.kAXMainAttribute: currentBoolValue = element.attribute(
|
||||
Attribute<Bool>(key),
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
default:
|
||||
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
|
||||
return false
|
||||
}
|
||||
|
||||
if let actualBool = currentBoolValue {
|
||||
let expectedBool = expectedValueString.lowercased() == "true"
|
||||
if actualBool != expectedBool {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' expected '\(expectedBool)', but found '\(actualBool)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
dLog(
|
||||
"attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func matchComputedNameAttributes(
|
||||
element: Element,
|
||||
computedNameEquals: String?,
|
||||
computedNameContains: String?,
|
||||
depth: Int,
|
||||
isDebugLoggingEnabled: Bool,
|
||||
currentDebugLogs: inout [String]
|
||||
) -> Bool {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
var tempLogs: [String] = [] // For Element method calls
|
||||
|
||||
if computedNameEquals == nil && computedNameContains == nil {
|
||||
return true // No computed name criteria to match, so this part passes.
|
||||
}
|
||||
|
||||
// getComputedAttributes will need logging parameters
|
||||
let computedAttrs = getComputedAttributes(
|
||||
for: element,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
)
|
||||
// Assuming AXMiscConstants.computedNameAttributeKey is a globally defined constant string for the key like "computedName"
|
||||
// And AttributeData has a `value: AnyCodable?` property
|
||||
if let currentComputedNameAnyCodable = computedAttrs[AXMiscConstants.computedNameAttributeKey]?.value as? AnyCodable,
|
||||
let currentComputedName = currentComputedNameAnyCodable.value as? String {
|
||||
if let equals = computedNameEquals {
|
||||
if currentComputedName != equals {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' != '\(equals)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if let contains = computedNameContains {
|
||||
if !currentComputedName.localizedCaseInsensitiveContains(contains) {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' does not contain '\(contains)'. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
// Only log failure if there was a criteria for computed name.
|
||||
if computedNameEquals != nil || computedNameContains != nil {
|
||||
dLog(
|
||||
"matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none or it's not a string. No match."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true // No criteria, and no computed name, effectively a pass for this sub-check.
|
||||
}
|
||||
}
|
||||
28
Sources/AXorcist/Utils/ErrorUtils.swift
Normal file
28
Sources/AXorcist/Utils/ErrorUtils.swift
Normal file
@ -0,0 +1,28 @@
|
||||
// ErrorUtils.swift - Error handling utilities
|
||||
|
||||
import ApplicationServices // Added for AXError type
|
||||
import Foundation
|
||||
|
||||
// Helper function to convert AXError to a string
|
||||
public func axErrorToString(_ error: AXError) -> String {
|
||||
switch error {
|
||||
case .success: return "success"
|
||||
case .failure: return "failure"
|
||||
case .apiDisabled: return "apiDisabled"
|
||||
case .invalidUIElement: return "invalidUIElement"
|
||||
case .invalidUIElementObserver: return "invalidUIElementObserver"
|
||||
case .cannotComplete: return "cannotComplete"
|
||||
case .attributeUnsupported: return "attributeUnsupported"
|
||||
case .actionUnsupported: return "actionUnsupported"
|
||||
case .notificationUnsupported: return "notificationUnsupported"
|
||||
case .notImplemented: return "notImplemented"
|
||||
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
|
||||
case .notificationNotRegistered: return "notificationNotRegistered"
|
||||
case .noValue: return "noValue"
|
||||
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
|
||||
case .notEnoughPrecision: return "notEnoughPrecision"
|
||||
case .illegalArgument: return "illegalArgument"
|
||||
@unknown default:
|
||||
return "unknown AXError (code: \(error.rawValue))"
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// String extension MOVED to String+HelperExtensions.swift
|
||||
// CustomCharacterSet struct MOVED to CustomCharacterSet.swift
|
||||
|
||||
// Scanner class from Scanner
|
||||
class Scanner {
|
||||
|
||||
|
||||
@ -29,3 +29,23 @@ extension Optional {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func truncated(to length: Int, trailing: String = "...") -> String {
|
||||
if self.count > length {
|
||||
return String(self.prefix(length - trailing.count)) + trailing
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
private static let MAX_LOG_ABBREV_LENGTH = 50
|
||||
|
||||
func truncatedToMaxLogAbbrev() -> String {
|
||||
if self.count > Self.MAX_LOG_ABBREV_LENGTH {
|
||||
return String(self.prefix(Self.MAX_LOG_ABBREV_LENGTH - 3)) + "..."
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,10 +22,10 @@ public func extractTextContent(
|
||||
dLog("Extracting text content for element: \(elementDescription)")
|
||||
var texts: [String] = []
|
||||
let textualAttributes = [
|
||||
kAXValueAttribute, kAXTitleAttribute, kAXDescriptionAttribute, kAXHelpAttribute,
|
||||
kAXPlaceholderValueAttribute, kAXLabelValueAttribute, kAXRoleDescriptionAttribute
|
||||
// Consider adding kAXStringForRangeParameterizedAttribute if dealing with large text views for performance
|
||||
// kAXSelectedTextAttribute could also be relevant depending on use case
|
||||
AXAttributeNames.kAXValueAttribute, AXAttributeNames.kAXTitleAttribute, AXAttributeNames.kAXDescriptionAttribute, AXAttributeNames.kAXHelpAttribute,
|
||||
AXAttributeNames.kAXPlaceholderValueAttribute, AXAttributeNames.kAXLabelValueAttribute, AXAttributeNames.kAXRoleDescriptionAttribute
|
||||
// Consider adding stringForRangeParameterizedAttribute if dealing with large text views for performance
|
||||
// selectedTextAttribute could also be relevant depending on use case
|
||||
]
|
||||
for attrName in textualAttributes {
|
||||
var tempLogs: [String] = [] // For the axValue call
|
||||
@ -35,7 +35,7 @@ public func extractTextContent(
|
||||
attr: attrName,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: &tempLogs
|
||||
), !strValue.isEmpty, strValue.lowercased() != kAXNotAvailableString.lowercased() {
|
||||
), !strValue.isEmpty, strValue.lowercased() != AXMiscConstants.kAXNotAvailableString.lowercased() {
|
||||
texts.append(strValue)
|
||||
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from axValue
|
||||
} else {
|
||||
|
||||
109
Sources/AXorcist/Values/AXValueSpecificFormatter.swift
Normal file
109
Sources/AXorcist/Values/AXValueSpecificFormatter.swift
Normal file
@ -0,0 +1,109 @@
|
||||
// AXValueSpecificFormatter.swift - Formats AXValue types into strings
|
||||
|
||||
import ApplicationServices
|
||||
import CoreGraphics // For CGPoint, CGSize etc.
|
||||
import Foundation
|
||||
|
||||
// Assumes ValueFormatOption is available (likely from ValueFormatter.swift or a shared Models.swift)
|
||||
// Assumes stringFromAXValueType is available from ValueHelpers.swift and axErrorToString is available from ErrorUtils.swift
|
||||
|
||||
@MainActor
|
||||
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .default) -> String {
|
||||
let type = AXValueGetType(axValue)
|
||||
|
||||
// Handle special boolean type first (raw value 4)
|
||||
// In some SDK versions or contexts, AXValueType might not have a direct .boolean case,
|
||||
// or a specific attribute might return a boolean-like value encoded with rawValue 4.
|
||||
if type.rawValue == 4 { // Check for boolean-like AXValue based on rawValue
|
||||
return formatBooleanAXValue(axValue, type: type, option: option)
|
||||
}
|
||||
|
||||
// Handle standard AXValue types
|
||||
return formatStandardAXValue(axValue, type: type, option: option)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatBooleanAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
|
||||
var boolResult: DarwinBoolean = false // Use DarwinBoolean for AXValueGetValue
|
||||
if AXValueGetValue(axValue, type, &boolResult) {
|
||||
let result = boolResult.boolValue ? "true" : "false"
|
||||
return option == .verbose ? "<Boolean: \(result)>" : result
|
||||
}
|
||||
// Fallback if AXValueGetValue fails
|
||||
return "AXValue (\(stringFromAXValueType(type)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatStandardAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
|
||||
switch type {
|
||||
case .cgPoint:
|
||||
return formatCGPointAXValue(axValue, option: option)
|
||||
case .cgSize:
|
||||
return formatCGSizeAXValue(axValue, option: option)
|
||||
case .cgRect:
|
||||
return formatCGRectAXValue(axValue, option: option)
|
||||
case .cfRange:
|
||||
return formatCFRangeAXValue(axValue, option: option)
|
||||
case .axError:
|
||||
return formatAXErrorAXValue(axValue, option: option)
|
||||
case .illegal:
|
||||
return "Illegal AXValue"
|
||||
@unknown default:
|
||||
// Use stringFromAXValueType for unknown cases
|
||||
return "AXValue (\(stringFromAXValueType(type)))"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGPointAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var point = CGPoint.zero
|
||||
if AXValueGetValue(axValue, .cgPoint, &point) {
|
||||
let result = "x=\(point.x) y=\(point.y)"
|
||||
return option == .verbose ? "<CGPoint: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgPoint)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGSizeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var size = CGSize.zero
|
||||
if AXValueGetValue(axValue, .cgSize, &size) {
|
||||
let result = "w=\(size.width) h=\(size.height)"
|
||||
return option == .verbose ? "<CGSize: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgSize)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGRectAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var rect = CGRect.zero
|
||||
if AXValueGetValue(axValue, .cgRect, &rect) {
|
||||
let result = "x=\(rect.origin.x) y=\(rect.origin.y) w=\(rect.size.width) h=\(rect.size.height)"
|
||||
return option == .verbose ? "<CGRect: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgRect)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCFRangeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var range = CFRange() // No .zero for CFRange, default init is fine.
|
||||
if AXValueGetValue(axValue, .cfRange, &range) {
|
||||
let result = "pos=\(range.location) len=\(range.length)"
|
||||
return option == .verbose ? "<CFRange: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cfRange)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatAXErrorAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var error = AXError.success
|
||||
if AXValueGetValue(axValue, .axError, &error) {
|
||||
let result = axErrorToString(error) // Assumes axErrorToString is available
|
||||
return option == .verbose ? "<AXError: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.axError)))"
|
||||
}
|
||||
|
||||
// stringFromAXValueType is available from ValueHelpers.swift
|
||||
|
||||
// axErrorToString is available from ErrorUtils.swift
|
||||
207
Sources/AXorcist/Values/ValueCasters.swift
Normal file
207
Sources/AXorcist/Values/ValueCasters.swift
Normal file
@ -0,0 +1,207 @@
|
||||
// ValueCasters.swift - Contains type casting helper functions for AX values
|
||||
|
||||
import ApplicationServices
|
||||
import CoreGraphics // For CGPoint, CGSize
|
||||
import Foundation
|
||||
|
||||
// Note: Assumes Element (for castToElementArray) is available.
|
||||
|
||||
@MainActor
|
||||
internal func castValueToType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
// Handle basic types
|
||||
if let result = castToBasicType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if let result = castToArrayType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle geometry types
|
||||
if let result = castToGeometryType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle special types
|
||||
if let result = castToSpecialType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
// Direct cast fallback
|
||||
if let directCast = value as? T {
|
||||
return directCast
|
||||
}
|
||||
|
||||
dLog(
|
||||
"axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. " +
|
||||
"Unwrapped value was \(type(of: value)): \(value)"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToBasicType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is String.Type:
|
||||
return castToString(value, attr: attr, dLog: dLog) as? T
|
||||
case is Bool.Type:
|
||||
return castToBool(value, attr: attr, dLog: dLog) as? T
|
||||
case is Int.Type:
|
||||
return castToInt(value, attr: attr, dLog: dLog) as? T
|
||||
case is Double.Type:
|
||||
return castToDouble(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToString(_ value: Any, attr: String, dLog: (String) -> Void) -> String? {
|
||||
if let str = value as? String {
|
||||
return str
|
||||
} else if let attrStr = value as? NSAttributedString {
|
||||
return attrStr.string
|
||||
}
|
||||
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToBool(_ value: Any, attr: String, dLog: (String) -> Void) -> Bool? {
|
||||
if let boolVal = value as? Bool {
|
||||
return boolVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.boolValue
|
||||
}
|
||||
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToInt(_ value: Any, attr: String, dLog: (String) -> Void) -> Int? {
|
||||
if let intVal = value as? Int {
|
||||
return intVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.intValue
|
||||
}
|
||||
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToDouble(_ value: Any, attr: String, dLog: (String) -> Void) -> Double? {
|
||||
if let doubleVal = value as? Double {
|
||||
return doubleVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.doubleValue
|
||||
}
|
||||
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToArrayType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is [AXUIElement].Type:
|
||||
return castToAXUIElementArray(value, attr: attr, dLog: dLog) as? T
|
||||
case is [Element].Type:
|
||||
return castToElementArray(value, attr: attr, dLog: dLog) as? T
|
||||
case is [String].Type:
|
||||
return castToStringArray(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToAXUIElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [AXUIElement]? {
|
||||
if let anyArray = value as? [Any?] {
|
||||
let result = anyArray.compactMap { item -> AXUIElement? in
|
||||
guard let cfItem = item else { return nil }
|
||||
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
|
||||
return (cfItem as! AXUIElement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
dLog("axValue: Expected [AXUIElement] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [Element]? {
|
||||
if let anyArray = value as? [Any?] {
|
||||
let result = anyArray.compactMap { item -> Element? in
|
||||
guard let cfItem = item else { return nil }
|
||||
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
|
||||
return Element(cfItem as! AXUIElement) // Assumes Element initializer is public/internal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
dLog("axValue: Expected [Element] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToStringArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [String]? {
|
||||
if let stringArray = value as? [Any?] {
|
||||
let result = stringArray.compactMap { $0 as? String }
|
||||
if result.count == stringArray.count {
|
||||
return result
|
||||
}
|
||||
}
|
||||
dLog("axValue: Expected [String] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToGeometryType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is CGPoint.Type:
|
||||
return castToCGPoint(value, attr: attr, dLog: dLog) as? T
|
||||
case is CGSize.Type:
|
||||
return castToCGSize(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToCGPoint(_ value: Any, attr: String, dLog: (String) -> Void) -> CGPoint? {
|
||||
if let pointVal = value as? CGPoint {
|
||||
return pointVal
|
||||
}
|
||||
dLog("axValue: Expected CGPoint for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToCGSize(_ value: Any, attr: String, dLog: (String) -> Void) -> CGSize? {
|
||||
if let sizeVal = value as? CGSize {
|
||||
return sizeVal
|
||||
}
|
||||
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToSpecialType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
if expectedType == AXUIElement.self {
|
||||
return castToAXUIElement(value, attr: attr, dLog: dLog) as? T
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
internal func castToAXUIElement(_ value: Any, attr: String, dLog: (String) -> Void) -> AXUIElement? {
|
||||
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
|
||||
return (cfValue as! AXUIElement)
|
||||
}
|
||||
let typeDescription = String(describing: type(of: value))
|
||||
let valueDescription = String(describing: value)
|
||||
dLog("axValue: Expected AXUIElement for attribute '\(attr)', but got \(typeDescription): \(valueDescription)")
|
||||
return nil
|
||||
}
|
||||
@ -11,101 +11,6 @@ public enum ValueFormatOption {
|
||||
// Add more variants as needed, like .minimal, .debug, etc.
|
||||
}
|
||||
|
||||
// MARK: - AXValue Formatting
|
||||
|
||||
@MainActor
|
||||
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .default) -> String {
|
||||
let type = AXValueGetType(axValue)
|
||||
|
||||
// Handle special boolean type first
|
||||
if type.rawValue == 4 {
|
||||
return formatBooleanAXValue(axValue, type: type, option: option)
|
||||
}
|
||||
|
||||
// Handle standard AXValue types
|
||||
return formatStandardAXValue(axValue, type: type, option: option)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatBooleanAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
|
||||
var boolResult: DarwinBoolean = false
|
||||
if AXValueGetValue(axValue, type, &boolResult) {
|
||||
let result = boolResult.boolValue ? "true" : "false"
|
||||
return option == .verbose ? "<Boolean: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(type)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatStandardAXValue(_ axValue: AXValue, type: AXValueType, option: ValueFormatOption) -> String {
|
||||
switch type {
|
||||
case .cgPoint:
|
||||
return formatCGPointAXValue(axValue, option: option)
|
||||
case .cgSize:
|
||||
return formatCGSizeAXValue(axValue, option: option)
|
||||
case .cgRect:
|
||||
return formatCGRectAXValue(axValue, option: option)
|
||||
case .cfRange:
|
||||
return formatCFRangeAXValue(axValue, option: option)
|
||||
case .axError:
|
||||
return formatAXErrorAXValue(axValue, option: option)
|
||||
case .illegal:
|
||||
return "Illegal AXValue"
|
||||
default:
|
||||
return "AXValue (\(stringFromAXValueType(type)))"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGPointAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var point = CGPoint.zero
|
||||
if AXValueGetValue(axValue, .cgPoint, &point) {
|
||||
let result = "x=\(point.x) y=\(point.y)"
|
||||
return option == .verbose ? "<CGPoint: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgPoint)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGSizeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var size = CGSize.zero
|
||||
if AXValueGetValue(axValue, .cgSize, &size) {
|
||||
let result = "w=\(size.width) h=\(size.height)"
|
||||
return option == .verbose ? "<CGSize: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgSize)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCGRectAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var rect = CGRect.zero
|
||||
if AXValueGetValue(axValue, .cgRect, &rect) {
|
||||
let result = "x=\(rect.origin.x) y=\(rect.origin.y) w=\(rect.size.width) h=\(rect.size.height)"
|
||||
return option == .verbose ? "<CGRect: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cgRect)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatCFRangeAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var range = CFRange()
|
||||
if AXValueGetValue(axValue, .cfRange, &range) {
|
||||
let result = "pos=\(range.location) len=\(range.length)"
|
||||
return option == .verbose ? "<CFRange: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.cfRange)))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func formatAXErrorAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
|
||||
var error = AXError.success
|
||||
if AXValueGetValue(axValue, .axError, &error) {
|
||||
let result = axErrorToString(error)
|
||||
return option == .verbose ? "<AXError: \(result)>" : result
|
||||
}
|
||||
return "AXValue (\(stringFromAXValueType(.axError)))"
|
||||
}
|
||||
|
||||
// MARK: - CFTypeRef Formatting
|
||||
|
||||
@MainActor
|
||||
@ -171,11 +76,11 @@ private func formatAXUIElement(
|
||||
currentDebugLogs: inout [String]
|
||||
) -> String {
|
||||
let element = Element(value as! AXUIElement)
|
||||
|
||||
|
||||
// Create a simple description using available element properties
|
||||
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "Unknown"
|
||||
let title = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
|
||||
if let title = title, !title.isEmpty {
|
||||
return option == .verbose ? "<\(role): \"\(title)\">" : "\(role):\"\(title)\""
|
||||
} else {
|
||||
@ -192,7 +97,7 @@ private func formatCFArray(
|
||||
) -> String {
|
||||
let cfArray = value as! CFArray
|
||||
let count = CFArrayGetCount(cfArray)
|
||||
|
||||
|
||||
if option == .verbose || count <= 5 {
|
||||
var swiftArray: [String] = []
|
||||
for index in 0..<count {
|
||||
@ -222,7 +127,7 @@ private func formatCFDictionary(
|
||||
) -> String {
|
||||
let cfDict = value as! CFDictionary
|
||||
let count = CFDictionaryGetCount(cfDict)
|
||||
|
||||
|
||||
if option == .verbose || count <= 3 {
|
||||
var swiftDict: [String: String] = [:]
|
||||
if let nsDict = cfDict as? [String: AnyObject] {
|
||||
|
||||
@ -3,7 +3,7 @@ import CoreGraphics // For CGPoint, CGSize etc.
|
||||
import Foundation
|
||||
|
||||
// debug() is assumed to be globally available from Logging.swift
|
||||
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
|
||||
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
|
||||
|
||||
// ValueUnwrapper has been moved to its own file: ValueUnwrapper.swift
|
||||
|
||||
@ -44,211 +44,10 @@ public func axValue<T>(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call castValueToType from ValueCasters.swift
|
||||
return castValueToType(value, expectedType: T.self, attr: attr, dLog: dLog)
|
||||
}
|
||||
|
||||
// MARK: - Type Casting Helpers
|
||||
|
||||
@MainActor
|
||||
private func castValueToType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
// Handle basic types
|
||||
if let result = castToBasicType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if let result = castToArrayType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle geometry types
|
||||
if let result = castToGeometryType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle special types
|
||||
if let result = castToSpecialType(value, expectedType: expectedType, attr: attr, dLog: dLog) {
|
||||
return result
|
||||
}
|
||||
// Direct cast fallback
|
||||
if let directCast = value as? T {
|
||||
return directCast
|
||||
}
|
||||
|
||||
dLog(
|
||||
"axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. " +
|
||||
"Unwrapped value was \(type(of: value)): \(value)"
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToBasicType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is String.Type:
|
||||
return castToString(value, attr: attr, dLog: dLog) as? T
|
||||
case is Bool.Type:
|
||||
return castToBool(value, attr: attr, dLog: dLog) as? T
|
||||
case is Int.Type:
|
||||
return castToInt(value, attr: attr, dLog: dLog) as? T
|
||||
case is Double.Type:
|
||||
return castToDouble(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToString(_ value: Any, attr: String, dLog: (String) -> Void) -> String? {
|
||||
if let str = value as? String {
|
||||
return str
|
||||
} else if let attrStr = value as? NSAttributedString {
|
||||
return attrStr.string
|
||||
}
|
||||
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToBool(_ value: Any, attr: String, dLog: (String) -> Void) -> Bool? {
|
||||
if let boolVal = value as? Bool {
|
||||
return boolVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.boolValue
|
||||
}
|
||||
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToInt(_ value: Any, attr: String, dLog: (String) -> Void) -> Int? {
|
||||
if let intVal = value as? Int {
|
||||
return intVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.intValue
|
||||
}
|
||||
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToDouble(_ value: Any, attr: String, dLog: (String) -> Void) -> Double? {
|
||||
if let doubleVal = value as? Double {
|
||||
return doubleVal
|
||||
} else if let numVal = value as? NSNumber {
|
||||
return numVal.doubleValue
|
||||
}
|
||||
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToArrayType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is [AXUIElement].Type:
|
||||
return castToAXUIElementArray(value, attr: attr, dLog: dLog) as? T
|
||||
case is [Element].Type:
|
||||
return castToElementArray(value, attr: attr, dLog: dLog) as? T
|
||||
case is [String].Type:
|
||||
return castToStringArray(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToAXUIElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [AXUIElement]? {
|
||||
if let anyArray = value as? [Any?] {
|
||||
let result = anyArray.compactMap { item -> AXUIElement? in
|
||||
guard let cfItem = item else { return nil }
|
||||
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
|
||||
return (cfItem as! AXUIElement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
dLog("axValue: Expected [AXUIElement] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToElementArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [Element]? {
|
||||
if let anyArray = value as? [Any?] {
|
||||
let result = anyArray.compactMap { item -> Element? in
|
||||
guard let cfItem = item else { return nil }
|
||||
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() {
|
||||
return Element(cfItem as! AXUIElement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
dLog("axValue: Expected [Element] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToStringArray(_ value: Any, attr: String, dLog: (String) -> Void) -> [String]? {
|
||||
if let stringArray = value as? [Any?] {
|
||||
let result = stringArray.compactMap { $0 as? String }
|
||||
if result.count == stringArray.count {
|
||||
return result
|
||||
}
|
||||
}
|
||||
dLog("axValue: Expected [String] for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToGeometryType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
switch expectedType {
|
||||
case is CGPoint.Type:
|
||||
return castToCGPoint(value, attr: attr, dLog: dLog) as? T
|
||||
case is CGSize.Type:
|
||||
return castToCGSize(value, attr: attr, dLog: dLog) as? T
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToCGPoint(_ value: Any, attr: String, dLog: (String) -> Void) -> CGPoint? {
|
||||
if let pointVal = value as? CGPoint {
|
||||
return pointVal
|
||||
}
|
||||
dLog("axValue: Expected CGPoint for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToCGSize(_ value: Any, attr: String, dLog: (String) -> Void) -> CGSize? {
|
||||
if let sizeVal = value as? CGSize {
|
||||
return sizeVal
|
||||
}
|
||||
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToSpecialType<T>(_ value: Any, expectedType: T.Type, attr: String, dLog: (String) -> Void) -> T? {
|
||||
if expectedType == AXUIElement.self {
|
||||
return castToAXUIElement(value, attr: attr, dLog: dLog) as? T
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func castToAXUIElement(_ value: Any, attr: String, dLog: (String) -> Void) -> AXUIElement? {
|
||||
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
|
||||
return (cfValue as! AXUIElement)
|
||||
}
|
||||
let typeDescription = String(describing: type(of: value))
|
||||
let valueDescription = String(describing: value)
|
||||
dLog("axValue: Expected AXUIElement for attribute '\(attr)', but got \(typeDescription): \(valueDescription)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - AXValueType String Helper
|
||||
|
||||
public func stringFromAXValueType(_ type: AXValueType) -> String {
|
||||
|
||||
@ -5,7 +5,7 @@ import CoreGraphics // For CGPoint, CGSize, CGRect, CFRange
|
||||
import Foundation
|
||||
|
||||
// debug() is assumed to be globally available from Logging.swift
|
||||
// Constants are assumed to be globally available from AccessibilityConstants.swift
|
||||
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
|
||||
// Scanner and CustomCharacterSet are from Scanner.swift
|
||||
// AccessibilityError is from AccessibilityError.swift
|
||||
|
||||
@ -90,7 +90,7 @@ public func createCFTypeRefFromString(stringValue: String, forElement element: E
|
||||
@MainActor
|
||||
private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValueType, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> AXValue? {
|
||||
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
|
||||
|
||||
|
||||
let valueRef: AXValue?
|
||||
switch targetAXValueType {
|
||||
case .cgPoint:
|
||||
@ -124,7 +124,7 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
|
||||
private func parseCGPoint(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
|
||||
var x: Double = 0, y: Double = 0
|
||||
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
|
||||
|
||||
|
||||
if components.count == 2,
|
||||
let xValStr = components[0].split(separator: "=").last, let xVal = Double(xValStr),
|
||||
let yValStr = components[1].split(separator: "=").last, let yVal = Double(yValStr) {
|
||||
@ -152,7 +152,7 @@ private func parseCGPoint(from stringValue: String, dLog: (String) -> Void) thro
|
||||
private func parseCGSize(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
|
||||
var w: Double = 0, h: Double = 0
|
||||
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
|
||||
|
||||
|
||||
if components.count == 2,
|
||||
let wValStr = components[0].split(separator: "=").last, let wVal = Double(wValStr),
|
||||
let hValStr = components[1].split(separator: "=").last, let hVal = Double(hValStr) {
|
||||
@ -180,7 +180,7 @@ private func parseCGSize(from stringValue: String, dLog: (String) -> Void) throw
|
||||
private func parseCGRect(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
|
||||
var x: Double = 0, y: Double = 0, w: Double = 0, h: Double = 0
|
||||
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
|
||||
|
||||
|
||||
if components.count == 4,
|
||||
let xStr = components[0].split(separator: "=").last, let xVal = Double(xStr),
|
||||
let yStr = components[1].split(separator: "=").last, let yVal = Double(yStr),
|
||||
@ -216,7 +216,7 @@ private func parseCGRect(from stringValue: String, dLog: (String) -> Void) throw
|
||||
private func parseCFRange(from stringValue: String, dLog: (String) -> Void) throws -> AXValue? {
|
||||
var loc: Int = 0, len: Int = 0
|
||||
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
|
||||
|
||||
|
||||
if components.count == 2,
|
||||
let locStr = components[0].split(separator: "=").last, let locVal = Int(locStr),
|
||||
let lenStr = components[1].split(separator: "=").last, let lenVal = Int(lenStr) {
|
||||
@ -245,10 +245,10 @@ private func parseCFRange(from stringValue: String, dLog: (String) -> Void) thro
|
||||
private func parseDefaultAXValueType(from stringValue: String, targetType: AXValueType, dLog: (String) -> Void) throws -> AXValue? {
|
||||
if targetType.rawValue == 4 {
|
||||
var boolVal: DarwinBoolean
|
||||
if stringValue.lowercased() == "true" {
|
||||
boolVal = true
|
||||
} else if stringValue.lowercased() == "false" {
|
||||
boolVal = false
|
||||
if stringValue.lowercased() == "true" {
|
||||
boolVal = true
|
||||
} else if stringValue.lowercased() == "false" {
|
||||
boolVal = false
|
||||
} else {
|
||||
dLog("parseStringToAXValue: Boolean parsing failed for '\(stringValue)' for AXValue.")
|
||||
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as boolean for AXValue.")
|
||||
|
||||
@ -3,7 +3,7 @@ import CoreGraphics // For CGPoint, CGSize etc.
|
||||
import Foundation
|
||||
|
||||
// debug() is assumed to be globally available from Logging.swift
|
||||
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
|
||||
// Accessibility constants are now available through namespaced enums like AXAttributeNames, AXRoleNames, etc.
|
||||
|
||||
// MARK: - ValueUnwrapper Utility
|
||||
struct ValueUnwrapper {
|
||||
|
||||
@ -20,8 +20,11 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
@Option(name: .long, help: "Read JSON payload from the specified file path.")
|
||||
var file: String?
|
||||
|
||||
@Option(name: .long, help: "Read JSON payload directly from this string argument, expecting a JSON string.")
|
||||
var json: String?
|
||||
|
||||
@Argument(
|
||||
help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file) are used, this argument is ignored."
|
||||
help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file, --json) are used, this argument is ignored."
|
||||
)
|
||||
var directPayload: String?
|
||||
|
||||
@ -30,6 +33,7 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
let inputResult = InputHandler.parseInput(
|
||||
stdin: stdin,
|
||||
file: file,
|
||||
json: json,
|
||||
directPayload: directPayload,
|
||||
debug: debug
|
||||
)
|
||||
@ -102,19 +106,33 @@ struct AXORCCommand: AsyncParsableCommand {
|
||||
print(result)
|
||||
|
||||
} catch {
|
||||
// FORCED DEBUGGING FOR THIS ERROR PATH
|
||||
// debug = true // Temporarily enable debug logs for this error block if needed
|
||||
|
||||
var errorSpecificDebugLogs = localDebugLogs // Copy existing logs
|
||||
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Original jsonString that led to this error: [\(jsonString)]")
|
||||
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: jsonData.count that led to this error: \(jsonData.count)")
|
||||
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Raw error.localizedDescription: \(error.localizedDescription)")
|
||||
errorSpecificDebugLogs.append("DECODE_ERROR_DEBUG: Full error object: \(error)")
|
||||
|
||||
let errorMessage = "Failed to parse JSON command. Raw Error: \(error.localizedDescription). JSON Input (first 100 chars): \(jsonString.prefix(100))..."
|
||||
|
||||
let errorResponse = ErrorResponse(
|
||||
command_id: "decode_error",
|
||||
error: ErrorResponse.ErrorDetail(
|
||||
message: "Failed to parse JSON command: \(error.localizedDescription)"
|
||||
message: errorMessage
|
||||
),
|
||||
debug_logs: debug ? localDebugLogs : nil
|
||||
// Always include these enhanced debug logs for decode_error for now
|
||||
debug_logs: errorSpecificDebugLogs
|
||||
)
|
||||
|
||||
if let jsonData = try? JSONEncoder().encode(errorResponse),
|
||||
let jsonStr = String(data: jsonData, encoding: .utf8) {
|
||||
print(jsonStr)
|
||||
if let responseData = try? JSONEncoder().encode(errorResponse),
|
||||
let responseStr = String(data: responseData, encoding: .utf8) {
|
||||
print(responseStr)
|
||||
} else {
|
||||
print("{\"error\": \"Failed to encode error response\"}")
|
||||
// Fallback if even error encoding fails
|
||||
let fallbackErrorMsg = "{\"error\": \"Failed to encode error response. Original error for decode: \(error.localizedDescription). Input was: \(jsonString)\"}"
|
||||
print(fallbackErrorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,171 +27,106 @@ struct CommandExecutor {
|
||||
|
||||
switch command.command {
|
||||
case .performAction:
|
||||
guard let actionName = command.action_name, let locator = command.locator else {
|
||||
let error = "Missing action_name or locator for performAction"
|
||||
guard let actionName = command.action_name else {
|
||||
let error = "Missing action_name for performAction"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(QueryResponse(
|
||||
success: false,
|
||||
commandId: command.command_id,
|
||||
command: command.command.rawValue,
|
||||
error: error,
|
||||
debugLogs: debug ? localDebugLogs : nil
|
||||
debugLogs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding error response failed\"}"
|
||||
}
|
||||
let handlerResponse: HandlerResponse = await ax.handlePerformAction(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
actionName: actionName,
|
||||
actionValue: command.action_value,
|
||||
maxDepth: command.max_elements,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executePerformAction(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs,
|
||||
actionName: actionName
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding performAction response failed\"}"
|
||||
|
||||
case .getFocusedElement:
|
||||
let handlerResponse: HandlerResponse = await ax.handleGetFocusedElement(
|
||||
for: command.application,
|
||||
requestedAttributes: command.attributes,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executeGetFocusedElement(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding getFocusedElement response failed\"}"
|
||||
|
||||
case .getAttributes:
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for getAttributes"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(QueryResponse(
|
||||
success: false,
|
||||
commandId: command.command_id,
|
||||
command: command.command.rawValue,
|
||||
error: error,
|
||||
debugLogs: debug ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding error response failed\"}"
|
||||
}
|
||||
let handlerResponse: HandlerResponse = await ax.handleGetAttributes(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
requestedAttributes: command.attributes,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executeGetAttributes(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding getAttributes response failed\"}"
|
||||
|
||||
case .query:
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for query"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(QueryResponse(
|
||||
success: false,
|
||||
commandId: command.command_id,
|
||||
command: command.command.rawValue,
|
||||
error: error,
|
||||
debugLogs: debug ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding error response failed\"}"
|
||||
}
|
||||
let handlerResponse: HandlerResponse = await ax.handleQuery(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
requestedAttributes: command.attributes,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executeQuery(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding query response failed\"}"
|
||||
|
||||
case .describeElement:
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for describeElement"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(QueryResponse(
|
||||
success: false,
|
||||
commandId: command.command_id,
|
||||
command: command.command.rawValue,
|
||||
error: error,
|
||||
debugLogs: debug ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding error response failed\"}"
|
||||
}
|
||||
let handlerResponse: HandlerResponse = await ax.handleDescribeElement(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executeDescribeElement(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding describeElement response failed\"}"
|
||||
|
||||
case .extractText:
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for extractText"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(QueryResponse(
|
||||
success: false,
|
||||
commandId: command.command_id,
|
||||
command: command.command.rawValue,
|
||||
error: error,
|
||||
debugLogs: debug ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding error response failed\"}"
|
||||
}
|
||||
let handlerResponse: HandlerResponse = await ax.handleExtractText(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
let handlerResponse = await Self.executeExtractText(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: handlerResponse.error == nil,
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding extractText response failed\"}"
|
||||
|
||||
case .collectAll:
|
||||
let jsonStringResult = await ax.handleCollectAll(
|
||||
@ -208,56 +143,239 @@ struct CommandExecutor {
|
||||
return jsonStringResult
|
||||
|
||||
case .batch:
|
||||
guard let subCommands = command.sub_commands else {
|
||||
let error = "Missing sub_commands for batch command"
|
||||
localDebugLogs.append(error)
|
||||
return encodeToJson(BatchResponse(
|
||||
command_id: command.command_id,
|
||||
success: false,
|
||||
results: [],
|
||||
error: error,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
)) ?? "{\"error\": \"Encoding batch error response failed\"}"
|
||||
}
|
||||
|
||||
var batchDebugLogs = localDebugLogs
|
||||
let batchResults: [HandlerResponse] = await ax.handleBatchCommands(
|
||||
batchCommandID: command.command_id,
|
||||
subCommands: subCommands,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &batchDebugLogs
|
||||
)
|
||||
|
||||
let overallSuccess = batchResults.allSatisfy { $0.error == nil }
|
||||
let batchResponse = BatchResponse(
|
||||
command_id: command.command_id,
|
||||
success: overallSuccess,
|
||||
results: batchResults,
|
||||
error: nil,
|
||||
debug_logs: effectiveDebugLogging ? batchDebugLogs : nil
|
||||
let batchResponse = await Self.executeBatch(
|
||||
command: command,
|
||||
ax: ax,
|
||||
effectiveDebugLogging: effectiveDebugLogging,
|
||||
localDebugLogs: &localDebugLogs
|
||||
)
|
||||
return encodeToJson(batchResponse) ?? "{\"error\": \"Encoding batch response failed\"}"
|
||||
|
||||
case .ping:
|
||||
if effectiveDebugLogging { localDebugLogs.append("Ping command received. Responding with pong.") }
|
||||
// Create an empty HandlerResponse for ping
|
||||
let pingHandlerResponse = HandlerResponse(
|
||||
data: nil,
|
||||
error: nil,
|
||||
debug_logs: nil // Ping-specific logs are already in localDebugLogs
|
||||
data: nil,
|
||||
error: nil,
|
||||
debug_logs: nil
|
||||
)
|
||||
// Construct QueryResponse using the HandlerResponse initializer
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: command.command_id,
|
||||
success: true, // Ping is always a success if reached
|
||||
command: command.command.rawValue,
|
||||
return Self.finalizeAndEncodeResponse(
|
||||
commandId: command.command_id,
|
||||
commandType: command.command.rawValue,
|
||||
handlerResponse: pingHandlerResponse,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
localDebugLogs: localDebugLogs,
|
||||
effectiveDebugLogging: effectiveDebugLogging
|
||||
)
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding ping response failed\"}"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command Execution Functions
|
||||
|
||||
private static func executePerformAction(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String],
|
||||
actionName: String
|
||||
) async -> HandlerResponse {
|
||||
// Locator is now optional for this path, AXorcist.handlePerformAction will use path_hint if locator is nil
|
||||
return await ax.handlePerformAction(
|
||||
for: command.application,
|
||||
locator: command.locator, // This can be nil
|
||||
pathHint: command.path_hint,
|
||||
actionName: actionName,
|
||||
actionValue: command.action_value,
|
||||
maxDepth: command.max_elements,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeGetFocusedElement(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
return await ax.handleGetFocusedElement(
|
||||
for: command.application,
|
||||
requestedAttributes: command.attributes,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeGetAttributes(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for getAttributes"
|
||||
localDebugLogs.append(error)
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: error,
|
||||
debug_logs: nil
|
||||
)
|
||||
}
|
||||
return await ax.handleGetAttributes(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
requestedAttributes: command.attributes,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeQuery(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for query"
|
||||
localDebugLogs.append(error)
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: error,
|
||||
debug_logs: nil
|
||||
)
|
||||
}
|
||||
return await ax.handleQuery(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
requestedAttributes: command.attributes,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeDescribeElement(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for describeElement"
|
||||
localDebugLogs.append(error)
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: error,
|
||||
debug_logs: nil
|
||||
)
|
||||
}
|
||||
return await ax.handleDescribeElement(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
maxDepth: command.max_elements,
|
||||
requestedAttributes: command.attributes,
|
||||
outputFormat: command.output_format,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeExtractText(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> HandlerResponse {
|
||||
guard let locator = command.locator else {
|
||||
let error = "Missing locator for extractText"
|
||||
localDebugLogs.append(error)
|
||||
return HandlerResponse(
|
||||
data: nil,
|
||||
error: error,
|
||||
debug_logs: nil
|
||||
)
|
||||
}
|
||||
return await ax.handleExtractText(
|
||||
for: command.application,
|
||||
locator: locator,
|
||||
pathHint: command.path_hint,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &localDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
private static func executeBatch(
|
||||
command: CommandEnvelope,
|
||||
ax: AXorcist,
|
||||
effectiveDebugLogging: Bool,
|
||||
localDebugLogs: inout [String]
|
||||
) async -> BatchResponse {
|
||||
guard let subCommands = command.sub_commands else {
|
||||
let error = "Missing sub_commands for batch command"
|
||||
localDebugLogs.append(error)
|
||||
return BatchResponse(
|
||||
command_id: command.command_id,
|
||||
success: false,
|
||||
results: [],
|
||||
error: error,
|
||||
debug_logs: effectiveDebugLogging ? localDebugLogs : nil
|
||||
)
|
||||
}
|
||||
|
||||
var batchDebugLogs = localDebugLogs
|
||||
let batchResults: [HandlerResponse] = await ax.handleBatchCommands(
|
||||
batchCommandID: command.command_id,
|
||||
subCommands: subCommands,
|
||||
isDebugLoggingEnabled: effectiveDebugLogging,
|
||||
currentDebugLogs: &batchDebugLogs
|
||||
)
|
||||
|
||||
let overallSuccess = batchResults.allSatisfy { $0.error == nil }
|
||||
return BatchResponse(
|
||||
command_id: command.command_id,
|
||||
success: overallSuccess,
|
||||
results: batchResults,
|
||||
error: nil,
|
||||
debug_logs: effectiveDebugLogging ? batchDebugLogs : nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private static func finalizeAndEncodeResponse(
|
||||
commandId: String,
|
||||
commandType: String,
|
||||
handlerResponse: HandlerResponse,
|
||||
localDebugLogs: [String],
|
||||
effectiveDebugLogging: Bool
|
||||
) -> String {
|
||||
// Combine debug logs if debug logging is enabled
|
||||
var combinedDebugLogs: [String]?
|
||||
if effectiveDebugLogging {
|
||||
combinedDebugLogs = localDebugLogs
|
||||
if let handlerDebugLogs = handlerResponse.debug_logs {
|
||||
combinedDebugLogs?.append(contentsOf: handlerDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
// Create QueryResponse
|
||||
let queryResponse = QueryResponse(
|
||||
command_id: commandId,
|
||||
success: handlerResponse.error == nil,
|
||||
command: commandType,
|
||||
handlerResponse: handlerResponse,
|
||||
debug_logs: combinedDebugLogs
|
||||
)
|
||||
|
||||
// Encode to JSON and return
|
||||
return encodeToJson(queryResponse) ?? "{\"error\": \"Encoding \(commandType) response failed\"}"
|
||||
}
|
||||
|
||||
private static func encodeToJson<T: Codable>(_ object: T) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
@ -272,4 +390,4 @@ struct CommandExecutor {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,48 +7,60 @@ struct InputHandler {
|
||||
static func parseInput(
|
||||
stdin: Bool,
|
||||
file: String?,
|
||||
json: String?,
|
||||
directPayload: String?,
|
||||
debug: Bool
|
||||
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
|
||||
|
||||
|
||||
var localDebugLogs: [String] = []
|
||||
if debug {
|
||||
localDebugLogs.append("Debug logging enabled by --debug flag.")
|
||||
}
|
||||
|
||||
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0)
|
||||
|
||||
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0) + (json != nil ? 1 : 0)
|
||||
let positionalPayloadProvided = directPayload != nil &&
|
||||
!(directPayload?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
|
||||
|
||||
|
||||
if activeInputFlags > 1 {
|
||||
return handleMultipleInputFlags(&localDebugLogs)
|
||||
} else if stdin {
|
||||
return handleStdinInput(debug: debug, debugLogs: &localDebugLogs)
|
||||
} else if let filePath = file {
|
||||
return handleFileInput(filePath: filePath, debug: debug, debugLogs: &localDebugLogs)
|
||||
} else if let jsonString = json {
|
||||
if debug {
|
||||
localDebugLogs.append("Using --json flag with payload of \\(jsonString.count) characters.")
|
||||
}
|
||||
let trimmedJsonString = jsonString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmedJsonString.isEmpty {
|
||||
let error = "Error: --json argument was provided but the string is empty or only whitespace."
|
||||
if debug { localDebugLogs.append(error) }
|
||||
return (nil, "--json argument", error, localDebugLogs)
|
||||
}
|
||||
return (trimmedJsonString, "--json argument", nil, localDebugLogs)
|
||||
} else if positionalPayloadProvided {
|
||||
return handleDirectPayload(directPayload: directPayload, debug: debug, debugLogs: &localDebugLogs)
|
||||
} else {
|
||||
return handleNoInput(debug: debug, debugLogs: &localDebugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
|
||||
private static func handleMultipleInputFlags(
|
||||
_ debugLogs: inout [String]
|
||||
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
|
||||
let error = "Error: Multiple input flags specified (--stdin, --file). Only one is allowed."
|
||||
let error = "Error: Multiple input flags specified (--stdin, --file, --json). Only one is allowed."
|
||||
return (nil, error, error, debugLogs)
|
||||
}
|
||||
|
||||
|
||||
private static func handleStdinInput(
|
||||
debug: Bool,
|
||||
debugLogs: inout [String]
|
||||
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
|
||||
let stdInputHandle = FileHandle.standardInput
|
||||
let stdinData = stdInputHandle.readDataToEndOfFile()
|
||||
|
||||
|
||||
if let str = String(data: stdinData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!str.isEmpty {
|
||||
if debug {
|
||||
@ -63,14 +75,14 @@ struct InputHandler {
|
||||
return (nil, "STDIN", error, debugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func handleFileInput(
|
||||
filePath: String,
|
||||
debug: Bool,
|
||||
debugLogs: inout [String]
|
||||
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
|
||||
let sourceDescription = "File: \(filePath)"
|
||||
|
||||
|
||||
do {
|
||||
let rawFileContent = try String(contentsOfFile: filePath, encoding: .utf8) // Read raw
|
||||
if debug {
|
||||
@ -81,7 +93,7 @@ struct InputHandler {
|
||||
if debug {
|
||||
debugLogs.append("HFI_DEBUG: Trimmed file content: '\(str)' (length: \(str.count))")
|
||||
}
|
||||
|
||||
|
||||
if !str.isEmpty {
|
||||
if debug {
|
||||
debugLogs.append("Successfully read \(str.count) characters from file: \(filePath)")
|
||||
@ -102,7 +114,7 @@ struct InputHandler {
|
||||
return (nil, sourceDescription, errorMsg, debugLogs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func handleDirectPayload(
|
||||
directPayload: String?,
|
||||
debug: Bool,
|
||||
@ -114,12 +126,12 @@ struct InputHandler {
|
||||
}
|
||||
return (jsonString, "Direct argument", nil, debugLogs)
|
||||
}
|
||||
|
||||
|
||||
private static func handleNoInput(
|
||||
debug: Bool,
|
||||
debugLogs: inout [String]
|
||||
) -> (jsonString: String?, sourceDescription: String, error: String?, debugLogs: [String]) {
|
||||
let error = "No input provided. Use --stdin, --file <path>, or provide JSON as a direct argument."
|
||||
let error = "No input provided. Use --stdin, --file <path>, --json <json_string>, or provide JSON as a direct argument."
|
||||
if debug {
|
||||
debugLogs.append("No input method specified and no direct payload provided.")
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import AppKit
|
||||
@testable import AXorcist
|
||||
@testable import AXorcistLib
|
||||
import Testing
|
||||
import XCTest
|
||||
|
||||
|
||||
78
axorc_runner.sh
Executable file
78
axorc_runner.sh
Executable file
@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Simple runner for axorc, taking a JSON file as input.
|
||||
# AXORC_PATH should be the path to your axorc executable.
|
||||
# If not set, it defaults to a path relative to this script.
|
||||
|
||||
# Determine the directory of this script
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# Set AXORC_PATH relative to the script's directory if not already set
|
||||
: ${AXORC_PATH:="$SCRIPT_DIR/AXorcist/.build/debug/axorc"}
|
||||
|
||||
# Check if AXORC_PATH exists and is executable
|
||||
if [ ! -x "$AXORC_PATH" ]; then
|
||||
echo "Error: axorc executable not found or not executable at $AXORC_PATH"
|
||||
echo "Please set AXORC_PATH environment variable or ensure it's built at the default location."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEBUG_FLAG=""
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
# Parse arguments for --debug and file/json payload
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--debug)
|
||||
DEBUG_FLAG="--debug"
|
||||
shift # past argument
|
||||
;;
|
||||
--file)
|
||||
if [[ -z "$2" || ! -f "$2" ]]; then
|
||||
echo "Error: File not provided or not found after --file argument."
|
||||
exit 1
|
||||
fi
|
||||
INPUT_JSON=$(cat "$2")
|
||||
USE_STDIN_FLAG=true
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--json)
|
||||
if [[ -z "$2" ]]; then
|
||||
echo "Error: JSON string not provided after --json argument."
|
||||
exit 1
|
||||
fi
|
||||
INPUT_JSON="$2"
|
||||
USE_STDIN_FLAG=true
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # unknown option will be captured if axorc supports more
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$INPUT_JSON" ]; then
|
||||
echo "Error: No JSON input provided via --file or --json."
|
||||
echo "Usage: $0 [--debug] --file /path/to/command.json OR $0 [--debug] --json '{"command":"ping"}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- DEBUG_RUNNER: INPUT_JSON content before piping --- BEGIN"
|
||||
printf "%s\n" "$INPUT_JSON"
|
||||
echo "--- DEBUG_RUNNER: INPUT_JSON content before piping --- END"
|
||||
echo "--- DEBUG_RUNNER: AXORC_PATH: $AXORC_PATH"
|
||||
echo "--- DEBUG_RUNNER: DEBUG_FLAG: $DEBUG_FLAG"
|
||||
|
||||
|
||||
# Execute axorc with the input JSON
|
||||
if [ "$USE_STDIN_FLAG" = true ]; then
|
||||
printf '%s' "$INPUT_JSON" | "$AXORC_PATH" --stdin $DEBUG_FLAG "${POSITIONAL_ARGS[@]}"
|
||||
AXORC_EXIT_CODE=$?
|
||||
echo "--- DEBUG_RUNNER: axorc exit code: $AXORC_EXIT_CODE ---"
|
||||
else
|
||||
# This case should not be reached if --file or --json is mandatory
|
||||
echo "Error: USE_STDIN_FLAG was not set, programming error in runner script."
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user