Add CI workflow and apply SwiftLint auto-fixes

- Configure GitHub Actions CI with build and SwiftLint checks
- Auto-fix SwiftLint formatting violations across codebase
- Enable automated code quality enforcement on PRs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-05-22 03:37:27 +02:00
parent d53304c3d7
commit a10bbaba1e
31 changed files with 792 additions and 790 deletions

36
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-lint:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Install SwiftLint
run: |
brew install swiftlint
- name: Build
run: |
swift build
- name: Run SwiftLint
run: |
swiftlint lint --strict
- name: Run Tests (would require accessibility permissions)
run: |
echo "⚠️ Tests require accessibility permissions and cannot run on CI"
echo "📝 Run 'make test' locally after granting accessibility permissions"

View File

@ -41,4 +41,4 @@ let package = Package(
// Sources will be inferred by SPM
)
]
)
)

View File

@ -80,7 +80,7 @@ public class AXorcist {
dLog("[AXorcist.handleGetFocusedElement] Failed to copy focused element attribute or it was nil. Status: \(axErrorToString(copyAttributeStatus)). Application: \(appIdentifier)")
return HandlerResponse(data: nil, error: "Could not get the focused UI element for \(appIdentifier). Ensure a window of the application is focused. AXError: \(axErrorToString(copyAttributeStatus))", debug_logs: currentDebugLogs)
}
guard CFGetTypeID(rawAXElement) == AXUIElementGetTypeID() else {
dLog("[AXorcist.handleGetFocusedElement] Focused element attribute was not an AXUIElement. Application: \(appIdentifier)")
return HandlerResponse(data: nil, error: "Focused element was not a valid UI element for \(appIdentifier).", debug_logs: currentDebugLogs)
@ -88,7 +88,7 @@ public class AXorcist {
let focusedElement = Element(rawAXElement as! AXUIElement)
dLog("[AXorcist.handleGetFocusedElement] Successfully obtained focused element: \(focusedElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) for application \(appIdentifier)")
let fetchedAttributes = getElementAttributes(
focusedElement,
requestedAttributes: requestedAttributes ?? [],
@ -98,9 +98,9 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let elementPathArray = focusedElement.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: fetchedAttributes, path: elementPathArray)
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
@ -180,7 +180,7 @@ public class AXorcist {
actualElementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute], // kAXRoleAttribute should be fine here
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
@ -188,10 +188,10 @@ public class AXorcist {
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = actualElementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(actualElementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)).")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
@ -255,7 +255,7 @@ public class AXorcist {
} 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
@ -275,9 +275,9 @@ public class AXorcist {
_ = 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
foundElement = search(
element: finalSearchTarget,
locator: locator,
@ -287,29 +287,29 @@ public class AXorcist {
currentDebugLogs: &currentDebugLogs
)
}
if let elementToQuery = foundElement {
let elementDescription = elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = elementDescription // Silences compiler warning
dLog("[AXorcist.handleQuery] Element found: \(elementDescription). Fetching attributes...")
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
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 {
@ -363,11 +363,11 @@ public class AXorcist {
_ = rootElementDescription // Silences compiler warning
dLog("[AXorcist.handleDescribeElement] Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)")
let foundElement = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
@ -375,25 +375,25 @@ public class AXorcist {
let elementDescription = elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = 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: false,
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: .verbose, // Describe implies verbose
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)).")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
@ -418,13 +418,13 @@ public class AXorcist {
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handlePerformAction] Handling for app: \(appIdentifier), action: \(actionName)")
@ -433,9 +433,9 @@ public class AXorcist {
dLog(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("[AXorcist.handlePerformAction] Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
@ -445,14 +445,14 @@ public class AXorcist {
}
effectiveElement = navigatedElement
}
dLog("[AXorcist.handlePerformAction] Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to find element with locator: \(locator)"
dLog(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
dLog("[AXorcist.handlePerformAction] Found element: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
if let actionValue = actionValue {
// Attempt to get a string representation of actionValue.value for logging
@ -462,10 +462,10 @@ public class AXorcist {
} else {
dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)'")
}
var errorMessage: String?
var axStatus: AXError = .success // Initialize to success
switch actionName.lowercased() {
case "press":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXPressAction as CFString)
@ -524,33 +524,33 @@ public class AXorcist {
// For other types, attempt a direct cast if possible, or log/error.
// This is a simplification; robust conversion is more involved.
if CFGetTypeID(actionValue.value as AnyObject) != 0 { // Basic check if it *might* be a CFType
cfValue = actionValue.value as AnyObject // bridge from Any to AnyObject then to CFTypeRef
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.")
cfValue = actionValue.value as AnyObject // bridge from Any to AnyObject then to CFTypeRef
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(foundElement.underlyingElement, actionName as CFString, finalCFValue)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(actionName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
}
} else if errorMessage == nil { // cfValue was nil, means conversion failed earlier but wasn't caught by the default error
errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type."
errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type."
}
} else {
errorMessage = "[AXorcist.handlePerformAction] Unknown action '\(actionName)' and no action_value provided to interpret as an attribute."
}
}
}
if let currentErrorMessage = errorMessage {
dLog(currentErrorMessage)
return HandlerResponse(data: nil, error: currentErrorMessage, debug_logs: currentDebugLogs)
}
dLog("[AXorcist.handlePerformAction] Action '\(actionName)' performed successfully.")
return HandlerResponse(data: nil, error: nil, debug_logs: currentDebugLogs)
}
@ -608,18 +608,18 @@ public class AXorcist {
if CFGetTypeID(value) == CFStringGetTypeID() {
extractedValueText = (value as! CFString) as String
if let extractedValueText = extractedValueText, !extractedValueText.isEmpty {
attributes["extractedValue"] = AnyCodable(extractedValueText)
dLog("Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
attributes["extractedValue"] = AnyCodable(extractedValueText)
dLog("Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
} else {
dLog("kAXValueAttribute was empty or not a string.")
}
} else {
dLog("kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(value))")
dLog("kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(value))")
}
} else {
dLog("Failed to get kAXValueAttribute or it was nil.")
}
cfValue = nil // Reset for next attribute
if AXUIElementCopyAttributeValue(foundElement.underlyingElement, kAXSelectedTextAttribute as CFString, &cfValue) == .success, let selectedValue = cfValue {
if CFGetTypeID(selectedValue) == CFStringGetTypeID() {
@ -677,7 +677,7 @@ public class AXorcist {
// or decide if currentDebugLogs should be directly mutated by sub-handlers and reflect cumulative logs.
// For simplicity here, let's assume sub-handlers append to the main currentDebugLogs.
dLog("Processing sub-command: \(subCmdID), type: \(subCommandEnvelope.command)", subCommandID: subCmdID)
var subCommandResponse: HandlerResponse
switch subCommandEnvelope.command {
@ -688,7 +688,7 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass the main log array
)
case .getAttributes:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for getAttributes in batch (sub-command ID: \(subCmdID))"
@ -706,7 +706,7 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .query:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for query in batch (sub-command ID: \(subCmdID))"
@ -724,7 +724,7 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .describeElement:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for describeElement in batch (sub-command ID: \(subCmdID))"
@ -741,7 +741,7 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .performAction:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for performAction in batch (sub-command ID: \(subCmdID))"
@ -765,7 +765,7 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .extractText:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for extractText in batch (sub-command ID: \(subCmdID))"
@ -780,21 +780,21 @@ public class AXorcist {
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .ping:
let pingMsg = "Ping command handled within batch (sub-command ID: \(subCmdID))"
dLog(pingMsg, subCommandID: subCmdID)
// For ping, the handlerResponse itself won't carry much data from AXorcist,
// For ping, the handlerResponse itself won't carry much data from AXorcist,
// but it should indicate success and carry the logs up to this point for this sub-command.
subCommandResponse = HandlerResponse(data: nil, error: nil, debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil)
// .batch command cannot be nested. .collectAll is also not handled by AXorcist lib directly.
case .collectAll, .batch:
let errorMsg = "Command type '\(subCommandEnvelope.command)' not supported within batch execution by AXorcist (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
// default case for any command types that might be added to CommandType enum
// default case for any command types that might be added to CommandType enum
// but not handled by this switch statement within handleBatchCommands.
// This is distinct from commands axorc itself might handle outside of AXorcist library.
// @unknown default: // This would be better if Swift enums allowed it easily here for non-frozen enums from other modules.
@ -866,14 +866,8 @@ public class AXorcist {
}
var collectedAXElements: [AXElement] = []
// The lines below were causing multiple errors and are removed.
// let maxDepth = commandEnvelope.maxDepth ?? defaultMaxDepthCollectAll
// let timeoutPerElement = commandEnvelope.timeoutPerElement ?? defaultTimeoutPerElementCollectAll
// var effectiveMaxDepth = (maxDepth > 0) ? maxDepth : defaultMaxDepthCollectAll
// dLog("Original effectiveMaxDepth from input maxDepth (\(maxDepth)): \(effectiveMaxDepth)", commandID: commandID, &recursiveCallDebugLogs)
//effectiveMaxDepth = 0 // <<<< FORCED FOR TEST >>>>
//dLog("TESTING: Forced effectiveMaxDepth to 0 to debug collection count", commandID: commandID, &recursiveCallDebugLogs)
let effectiveMaxDepth = maxDepth ?? 8
dLog("Max collection depth: \(effectiveMaxDepth)")
var collectRecursively: ((AXUIElement, Int) -> Void)!
collectRecursively = { axUIElement, currentDepth in
@ -882,10 +876,10 @@ public class AXorcist {
dLog("Reached recursionDepthLimit (\(recursionDepthLimit)) at element \(Element(axUIElement).briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)), stopping recursion for this branch.")
return
}
let currentElement = Element(axUIElement)
var shouldIncludeElement = true
var shouldIncludeElement = true
// If we are at depth 0 (the start element itself) AND a locator was provided,
// then this start element must match the locator.
// For all children (depth > 0), or if no locator was provided at all,
@ -913,35 +907,34 @@ public class AXorcist {
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth) is a child of a located start element. Including it regardless of initial locator criteria.")
}
// If locator was nil initially, shouldIncludeElement remains true.
if shouldIncludeElement {
dLog("Collecting element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)")
let fetchedAttrs = getElementAttributes(
currentElement,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: true,
forMultiDefault: true,
targetRole: nil as String?,
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs // Pass self.recursiveCallDebugLogs
)
let elementPath = currentElement.generatePathArray(
upTo: appElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs // Pass self.recursiveCallDebugLogs
)
let axElement = AXElement(attributes: fetchedAttrs, path: elementPath)
collectedAXElements.append(axElement)
} else if locator != nil {
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) did not match locator. Still checking children.")
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) did not match locator. Still checking children.")
}
var childrenRef: CFTypeRef?
let childrenResult = AXUIElementCopyAttributeValue(axUIElement, kAXChildrenAttribute as CFString, &childrenRef)
if childrenResult == .success, let children = childrenRef as? [AXUIElement] {
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) has \(children.count) children at depth \(currentDepth). Recursing.")
for childElement in children {
@ -950,7 +943,7 @@ public class AXorcist {
} else if childrenResult != .success {
dLog("Failed to get children for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)): \(axErrorToString(childrenResult))")
} else {
dLog("No children found for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)")
dLog("No children found for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)")
}
}
@ -969,4 +962,4 @@ public class AXorcist {
return HandlerResponse(data: responseDataElement, error: nil, debug_logs: self.recursiveCallDebugLogs)
}
}
}

View File

@ -30,7 +30,7 @@ public func handleGetAttributes(cmd: CommandEnvelope, isDebugLoggingEnabled: Boo
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
guard let locator = cmd.locator else {
let errorMessage = "Locator not provided for get_attributes."
dLog("handleGetAttributes: \(errorMessage)")
@ -39,11 +39,11 @@ public func handleGetAttributes(cmd: CommandEnvelope, isDebugLoggingEnabled: Boo
dLog("handleGetAttributes: Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs))")
let foundElement = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: cmd.max_elements ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: cmd.max_elements ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &handlerLogs
)
@ -52,7 +52,7 @@ public func handleGetAttributes(cmd: CommandEnvelope, isDebugLoggingEnabled: Boo
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: cmd.attributes ?? [],
forMultiDefault: false,
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: cmd.output_format ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -68,4 +68,4 @@ public func handleGetAttributes(cmd: CommandEnvelope, isDebugLoggingEnabled: Boo
dLog("handleGetAttributes: \(errorMessage)")
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
}

View File

@ -1,15 +1,15 @@
import Foundation
import ApplicationServices
import AppKit
import AppKit
// Note: Relies on applicationElement, navigateToElement, search, getElementAttributes,
// Note: Relies on applicationElement, navigateToElement, search, getElementAttributes,
// DEFAULT_MAX_DEPTH_SEARCH, CommandEnvelope, QueryResponse, Locator.
@MainActor
public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async throws -> QueryResponse {
var handlerLogs: [String] = [] // Local logs for this handler
func dLog(_ message: String) { if isDebugLoggingEnabled { handlerLogs.append(message) } }
let appIdentifier = cmd.application ?? focusedApplicationKey
dLog("Handling query for app: \(appIdentifier)")
@ -28,7 +28,7 @@ public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Element not found via path hint: \(pathHint.joined(separator: " -> "))", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
guard let locator = cmd.locator else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Locator not provided in command.", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
@ -37,7 +37,7 @@ public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async
let criteriaKeys = locator.criteria.keys
let isAppOnlyLocator = criteriaKeys.allSatisfy { appSpecifiers.contains($0) } && criteriaKeys.count == 1
var foundElement: Element? = nil
var foundElement: Element?
if isAppOnlyLocator {
dLog("Locator is app-only (criteria: \(locator.criteria)). Using appElement directly.")
@ -57,9 +57,9 @@ public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async
searchStartElementForLocator = effectiveElement
dLog("Searching with locator from element (determined by main path_hint or app root): \(searchStartElementForLocator.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs))")
}
let finalSearchTarget = (cmd.path_hint != nil && !cmd.path_hint!.isEmpty) ? effectiveElement : searchStartElementForLocator
// Pass logging parameters to search
foundElement = search(
element: finalSearchTarget,
@ -70,13 +70,13 @@ public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async
currentDebugLogs: &handlerLogs
)
}
if let elementToQuery = foundElement {
// Pass logging parameters to getElementAttributes
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: cmd.attributes ?? [],
forMultiDefault: false,
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: cmd.output_format ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
@ -89,4 +89,4 @@ public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async
} else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "No element matches single query criteria with locator or app-only locator failed to resolve.", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
}

View File

@ -198,4 +198,4 @@ public let focusedApplicationKey = "focused"
public let computedNameAttributeKey = "ComputedName"
public let isClickableAttributeKey = "IsClickable"
public let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
public let computedPathAttributeKey = "ComputedPath"
public let computedPathAttributeKey = "ComputedPath"

View File

@ -61,7 +61,7 @@ public enum AccessibilityError: Error, CustomStringConvertible {
if let m = msg { return "\(base) \(m)" }
return base
case .invalidElement: return "The specified UI element is invalid (possibly stale)."
// Attribute Errors
case .attributeUnsupported(let attr): return "Attribute '\(attr)' is not supported by this element."
case .attributeNotReadable(let attr): return "Attribute '\(attr)' is not readable."
@ -80,7 +80,7 @@ public enum AccessibilityError: Error, CustomStringConvertible {
// Generic & System
case .unknownAXError(let e): return "An unexpected Accessibility Framework error occurred: \(e)."
case .jsonEncodingFailed(let err):
case .jsonEncodingFailed(let err):
let base = "Failed to encode the response to JSON."
if let e = err { return "\(base) Error: \(e.localizedDescription)" }
return base
@ -105,4 +105,4 @@ public enum AccessibilityError: Error, CustomStringConvertible {
case .unknownAXError, .genericError: return 1
}
}
}
}

View File

@ -30,17 +30,17 @@ public struct AXPermissionsStatus {
public func checkAccessibilityPermissions(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws {
// Define local dLog using passed-in parameters
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let trustedOptions = [kAXTrustedCheckOptionPromptKey: true] as CFDictionary
// tempLogs is already declared for getParentProcessName, which is good.
// var tempLogs: [String] = [] // This would be a re-declaration error if uncommented
if !AXIsProcessTrustedWithOptions(trustedOptions) {
if !AXIsProcessTrustedWithOptions(trustedOptions) {
// Use isDebugLoggingEnabled for the call to getParentProcessName
let parentName = getParentProcessName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let parentName = getParentProcessName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let errorDetail = parentName != nil ? "Hint: Grant accessibility permissions to '\(parentName!)'." : "Hint: Ensure the application running this tool has Accessibility permissions."
dLog("Accessibility check failed (AXIsProcessTrustedWithOptions returned false). Details: \(errorDetail)")
throw AccessibilityError.notAuthorized(errorDetail)
throw AccessibilityError.notAuthorized(errorDetail)
} else {
dLog("Accessibility permissions are granted (AXIsProcessTrustedWithOptions returned true).")
}
@ -50,7 +50,7 @@ public func checkAccessibilityPermissions(isDebugLoggingEnabled: Bool, currentDe
public func getPermissionsStatus(checkAutomationFor bundleIDs: [String] = [], isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AXPermissionsStatus {
// Local dLog appends to currentDebugLogs
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Starting full permission status check.")
// Check overall accessibility API status and process trust
@ -77,7 +77,7 @@ public func getPermissionsStatus(checkAutomationFor bundleIDs: [String] = [], is
let scriptSource = """
tell application id \"\(bundleID)\" to count windows
"""
var errorDict: NSDictionary? = nil
var errorDict: NSDictionary?
if let script = NSAppleScript(source: scriptSource) {
if isDebugLoggingEnabled { dLog("Executing AppleScript against \(bundleID) to check automation status.") }
let descriptor = script.executeAndReturnError(&errorDict) // descriptor is non-optional
@ -109,10 +109,10 @@ public func getPermissionsStatus(checkAutomationFor bundleIDs: [String] = [], is
let finalStatus = AXPermissionsStatus(
isAccessibilityApiEnabled: isProcessTrusted, // Base this on isProcessTrusted now
isProcessTrustedForAccessibility: isProcessTrusted,
automationStatus: automationStatus,
isProcessTrustedForAccessibility: isProcessTrusted,
automationStatus: automationStatus,
overallErrorMessages: currentDebugLogs // All logs collected so far become the messages
)
dLog("Finished permission status check. isAccessibilityApiEnabled: \(finalStatus.isAccessibilityApiEnabled), isProcessTrusted: \(finalStatus.isProcessTrustedForAccessibility)")
return finalStatus
}
}

View File

@ -50,7 +50,7 @@ public struct Attribute<T> {
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMainWindowAttribute) } // Can be nil
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedWindowAttribute) } // Can be nil
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(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) }
@ -110,4 +110,4 @@ public struct Attribute<T> {
// For now, relying on position and size.
// Add more attributes as needed from ApplicationServices/HIServices Accessibility Attributes...
}
}

View File

@ -4,7 +4,7 @@ import ApplicationServices
// MARK: - Element Hierarchy Logic
extension Element {
@MainActor
@MainActor
public func children(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var collectedChildren: [Element] = []
@ -53,7 +53,7 @@ extension Element {
currentDebugLogs.append(contentsOf: tempLogs)
}
}
tempLogs.removeAll()
let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
@ -61,8 +61,8 @@ extension Element {
if currentRole == kAXApplicationRole as String {
tempLogs.removeAll()
if let windowElementsUI: [AXUIElement] = attribute(Attribute<[AXUIElement]>.windows, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
currentDebugLogs.append(contentsOf: tempLogs)
for childUI in windowElementsUI {
currentDebugLogs.append(contentsOf: tempLogs)
for childUI in windowElementsUI {
let childAX = Element(childUI)
if !uniqueChildrenSet.contains(childAX) {
collectedChildren.append(childAX)
@ -84,4 +84,4 @@ extension Element {
}
// generatePathString() is now fully implemented in Element.swift
}
}

View File

@ -5,43 +5,43 @@ import ApplicationServices
extension Element {
// Common Attribute Getters - now methods to accept logging parameters
@MainActor public func role(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.role, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func role(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.role, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func subrole(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.subrole, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func subrole(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.subrole, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func title(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.title, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func title(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.title, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func description(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.description, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func description(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.description, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isEnabled(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.enabled, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func isEnabled(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.enabled, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func value(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Any? {
attribute(Attribute<Any>.value, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func value(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Any? {
attribute(Attribute<Any>.value, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func roleDescription(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.roleDescription, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func roleDescription(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.roleDescription, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func help(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.help, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func help(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.help, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func identifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.identifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func identifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.identifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
// Status Properties - now methods
@MainActor public func isFocused(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.focused, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func isFocused(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.focused, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isHidden(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func isHidden(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isElementBusy(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.busy, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
@MainActor public func isElementBusy(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.busy, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isIgnored(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
@ -89,10 +89,10 @@ extension Element {
guard let elementUI: AXUIElement = attribute(Attribute<AXUIElement?>.focusedElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? nil else { return nil }
return Element(elementUI)
}
// Action-related - now a method
@MainActor
public func supportedActions(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [String]? {
return attribute(Attribute<[String]>.actionNames, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
}
}

View File

@ -43,11 +43,11 @@ public struct Element: Equatable, Hashable {
let error = AXUIElementCopyAttributeValue(self.underlyingElement, attributeName as CFString, &value)
if error == .success {
return value // Caller is responsible for CFRelease if it's a new object they own.
// For many get operations, this is a copy-get rule, but some are direct gets.
// Since we just return it, the caller should be aware or this function should manage it.
// Given AXSwift patterns, often the raw value isn't directly exposed like this,
// or it is clearly documented. For now, let's assume this is for internal use by attributesMatch
// which previously used copyAttributeValue which likely returned a +1 ref count object.
// For many get operations, this is a copy-get rule, but some are direct gets.
// Since we just return it, the caller should be aware or this function should manage it.
// Given AXSwift patterns, often the raw value isn't directly exposed like this,
// or it is clearly documented. For now, let's assume this is for internal use by attributesMatch
// which previously used copyAttributeValue which likely returned a +1 ref count object.
} else if error == .attributeUnsupported {
dLog("rawAttributeValue: Attribute \(attributeName) unsupported for element \(self.underlyingElement)")
} else if error == .noValue {
@ -142,18 +142,17 @@ public struct Element: Equatable, Hashable {
}
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: &currentDebugLogs)
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 }
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 {
@ -179,19 +178,19 @@ public struct Element: Equatable, Hashable {
/// This provides a general-purpose, human-readable name.
@MainActor
// Convert from a computed property to a method to accept logging parameters
public func computedName(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
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: &currentDebugLogs), !titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr }
if let valueStr: String = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) as? String, !valueStr.isEmpty, valueStr != kAXNotAvailableString { return valueStr }
if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr }
if let helpStr: String = self.attribute(Attribute<String>(kAXHelpAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr }
if let phValueStr: String = self.attribute(Attribute<String>(kAXPlaceholderValueAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !phValueStr.isEmpty, phValueStr != kAXNotAvailableString { return phValueStr }
let roleNameStr: String = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "Element"
if let roleDescStr: String = self.roleDescription(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !roleDescStr.isEmpty, roleDescStr != kAXNotAvailableString {
return "\(roleDescStr) (\(roleNameStr))"
}
@ -259,27 +258,27 @@ extension Element {
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
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 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 {
@ -300,7 +299,7 @@ extension Element {
var currentElement: Element? = self
var depth = 0
let maxDepth = 25
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")")
@ -320,27 +319,27 @@ extension Element {
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
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 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 {
@ -352,4 +351,4 @@ extension Element {
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
return reversedPathComponents
}
}
}

View File

@ -126,17 +126,17 @@ public struct CommandEnvelope: Codable {
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,
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
) {
@ -171,7 +171,7 @@ public struct Locator: Codable {
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
@ -249,7 +249,6 @@ public struct TextContentResponse: Codable {
}
}
// Generic error response
public struct ErrorResponse: Codable {
public var command_id: String
@ -267,7 +266,7 @@ public struct ErrorResponse: Codable {
public struct ErrorDetail: Codable {
public var message: String
public init(message: String) {
self.message = message
}
@ -302,4 +301,4 @@ public struct AXElement: Codable {
self.attributes = attributes
self.path = path
}
}
}

View File

@ -75,7 +75,7 @@ public func pid(forAppIdentifier ident: String, isDebugLoggingEnabled: Bool, cur
} else {
dLog("ProcessUtils: Identifier '\(ident)' is not a valid file path or bundle info could not be read.")
}
dLog("ProcessUtils: Trying by interpreting '\(ident)' as a PID string.")
if let pidInt = Int32(ident) {
if let appByPid = NSRunningApplication(processIdentifier: pidInt), !appByPid.isTerminated {
@ -117,4 +117,4 @@ public func getParentProcessName(isDebugLoggingEnabled: Bool, currentDebugLogs:
}
dLog("ProcessUtils: Could not get NSRunningApplication for parent PID \(parentPid).")
return nil
}
}

View File

@ -4,7 +4,7 @@ import Foundation
import ApplicationServices // For AXUIElement related types
import CoreGraphics // For potential future use with geometry types from attributes
// Note: This file assumes Models (for ElementAttributes, AnyCodable),
// Note: This file assumes Models (for ElementAttributes, AnyCodable),
// Logging (for debug), AccessibilityConstants, and Utils (for axValue) are available in the same module.
// And now Element for the new element wrapper.
@ -32,7 +32,7 @@ private func extractDirectPropertyValue(for attributeName: String, from element:
var tempLogs: [String] = [] // For Element method calls
var extractedValue: Any?
var handled = true
// Ensure logging parameters are passed to Element methods
switch attributeName {
case kAXPathHintAttribute:
@ -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 == kAXParentAttribute {
tempCallLogs.removeAll()
let parent = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // Directly assign AnyCodable
currentDebugLogs.append(contentsOf: tempCallLogs)
continue
} else if attr == kAXChildrenAttribute {
result[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 {
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 == 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
@ -149,24 +149,24 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
currentDebugLogs.append(contentsOf: tempCallLogs)
if outputFormat == .text_content {
finalValueToStore = formatRawCFValueForTextContent(rawCFValue)
} else {
} else {
finalValueToStore = formatCFTypeRef(rawCFValue, option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
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) {
strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == kAXNotAvailableString {
dLog("Smart format: Skipping attribute '\(attr)' with unhelpful value: \(strVal)")
continue
continue
}
}
result[attr] = AnyCodable(finalValueToStore)
}
tempLogs.removeAll()
if result[computedNameAttributeKey] == nil {
if result[computedNameAttributeKey] == nil {
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
result[computedNameAttributeKey] = AnyCodable(name)
dLog("Added ComputedName: \(name)")
@ -175,16 +175,16 @@ public func getElementAttributes(_ element: Element, requestedAttributes: [Strin
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if result[isClickableAttributeKey] == nil {
if result[isClickableAttributeKey] == nil {
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if isButton || hasPressAction {
if isButton || hasPressAction {
result[isClickableAttributeKey] = AnyCodable(true)
dLog("Added IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if outputFormat == .verbose && result[computedPathAttributeKey] == nil {
let path = element.generatePathString(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
@ -229,8 +229,7 @@ private func populateActionNamesAttribute(for element: Element, result: inout El
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 = [kAXPressAction] } else if !actionsToStore!.contains(kAXPressAction) { actionsToStore!.append(kAXPressAction) }
}
if let finalActions = actionsToStore, !finalActions.isEmpty {
@ -310,7 +309,7 @@ public func encodeAttributesToJSONStringRepresentation(_ attributes: ElementAttr
do {
let jsonData = try encoder.encode(attributes) // attributes is [String: AnyCodable]
if let jsonString = String(data: jsonData, encoding: .utf8) {
return ["json_representation": AnyCodable(jsonString)]
return ["json_representation": AnyCodable(jsonString)]
} else {
return ["error": AnyCodable("Failed to convert encoded JSON data to string")]
}
@ -350,7 +349,7 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
attributes[isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
dLog("IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
}
// Ensure other computed attributes like ComputedPath also use methods with logging if they exist.
// For now, this focuses on the direct errors.
@ -364,14 +363,10 @@ public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: B
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() {
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")>" }
} 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.
// Any other attribute-specific helper functions could go here in the future.

View File

@ -28,7 +28,7 @@ internal func attributesMatch(element: Element, matchDetails: [String: String],
}
continue
}
if key == kAXActionNamesAttribute || key == kAXAllowedValuesAttribute || key == kAXChildrenAttribute {
if !matchArrayAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return false
@ -76,16 +76,16 @@ internal func matchArrayAttribute(element: Element, key: String, expectedValueSt
dLog("matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match.")
return false
}
var actualArray: [String]? = nil
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
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?.map { childElement -> String in
var childLogs: [String] = []
return childElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
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.")
@ -121,7 +121,7 @@ internal func matchBooleanAttribute(element: Element, key: String, expectedValue
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:
default:
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
return false
}
@ -133,7 +133,7 @@ internal func matchBooleanAttribute(element: Element, key: String, expectedValue
return false
}
return true
} else {
} else {
dLog("attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match.")
return false
}
@ -165,9 +165,8 @@ internal func matchComputedNameAttributes(element: Element, computedNameEquals:
}
}
return true
} else {
} else {
dLog("matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none. No match.")
return false
}
}

View File

@ -17,7 +17,7 @@ enum ElementMatchStatus {
@MainActor
internal func evaluateElementAgainstCriteria(element: Element, locator: Locator, actionToVerify: String?, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> ElementMatchStatus {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods that need their own log scope temporarily
let currentElementRoleForLog: String? = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
@ -56,18 +56,18 @@ internal func evaluateElementAgainstCriteria(element: Element, locator: Locator,
} else {
dLog("evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched. No action to verify or action already included in locator.criteria for attributesMatch.")
}
return .fullMatch
}
@MainActor
public func search(element: Element,
locator: Locator,
requireAction: String?,
depth: Int = 0,
maxDepth: Int = DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
locator: Locator,
requireAction: String?,
depth: Int = 0,
maxDepth: Int = DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods
@ -82,19 +82,19 @@ public func search(element: Element,
return nil
}
let matchStatus = evaluateElementAgainstCriteria(element: element,
locator: locator,
actionToVerify: requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
let matchStatus = evaluateElementAgainstCriteria(element: element,
locator: locator,
actionToVerify: requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
if matchStatus == .fullMatch {
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
dLog("search [D\(depth)]: evaluateElementAgainstCriteria returned .fullMatch for \(briefDesc). Returning element.")
return element
}
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if matchStatus == .partialMatch_actionMissing {
dLog("search [D\(depth)]: Element \(briefDesc) matched criteria but missed action '\(requireAction ?? "")'. Continuing child search.")
@ -106,11 +106,11 @@ public func search(element: Element,
let childrenToSearch: [Element] = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
if !childrenToSearch.isEmpty {
for childElement in childrenToSearch {
if let found = search(element: childElement, locator: locator, requireAction: requireAction, depth: depth + 1, maxDepth: maxDepth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return found
}
}
for childElement in childrenToSearch {
if let found = search(element: childElement, locator: locator, requireAction: requireAction, depth: depth + 1, maxDepth: maxDepth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return found
}
}
}
return nil
}
@ -154,18 +154,18 @@ public func collectAll(
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: &currentDebugLogs) // Pass through logs
let matchStatus = evaluateElementAgainstCriteria(element: currentElement,
locator: locator,
actionToVerify: locator.requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
if matchStatus == .fullMatch {
if foundElements.count < maxElements {
if !foundElements.contains(currentElement) {
foundElements.append(currentElement)
dLog("collectAll [D\(depth)]: Added \(briefDescCurrent). Hits: \(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.")
}
@ -179,22 +179,22 @@ public func collectAll(
let newPath = currentPath + [currentElement]
for child in childrenToExplore {
if foundElements.count >= maxElements {
if foundElements.count >= maxElements {
dLog("collectAll [D\(depth)]: Max elements (\(maxElements)) reached during child traversal of \(briefDescCurrent). Stopping further exploration for this branch.")
break
break
}
collectAll(
appElement: appElement,
appElement: appElement,
locator: locator,
currentElement: child,
depth: depth + 1,
maxDepth: maxDepth,
currentElement: child,
depth: depth + 1,
maxDepth: maxDepth,
maxElements: maxElements,
currentPath: newPath,
elementsBeingProcessed: &elementsBeingProcessed,
currentPath: newPath,
elementsBeingProcessed: &elementsBeingProcessed,
foundElements: &foundElements,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass through logs
)
}
}
}

View File

@ -31,19 +31,19 @@ public func navigateToElement(from rootElement: Element, pathHint: [String], isD
dLog("Failed to parse path component: \(pathComponent)")
return nil
}
var tempBriefDescLogs: [String] = [] // Placeholder for briefDescription logs
if role.lowercased() == "window" || role.lowercased() == kAXWindowRole.lowercased() {
if role.lowercased() == "window" || role.lowercased() == kAXWindowRole.lowercased() {
guard let windowUIElements: [AXUIElement] = axValue(of: currentElement.underlyingElement, attr: kAXWindowsAttribute, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("PathUtils: AXWindows attribute could not be fetched as [AXUIElement].")
return nil
}
dLog("PathUtils: Fetched \(windowUIElements.count) AXUIElements for AXWindows.")
let windows: [Element] = windowUIElements.map { Element($0) }
dLog("PathUtils: Mapped to \(windows.count) Elements.")
guard index < windows.count else {
dLog("PathUtils: Index \(index) is out of bounds for windows array (count: \(windows.count)). Component: \(pathComponent).")
return nil
@ -61,15 +61,15 @@ public func navigateToElement(from rootElement: Element, pathHint: [String], isD
dLog("PathUtils: Mapped to \(allChildren.count) Elements for children of \(currentElementDesc) for \(pathComponent).")
guard !allChildren.isEmpty else {
dLog("No children found for element \(currentElementDesc) while processing component: \(pathComponent)")
return nil
dLog("No children found for element \(currentElementDesc) while processing component: \(pathComponent)")
return nil
}
let matchingChildren = allChildren.filter {
let matchingChildren = allChildren.filter {
guard let childRole: String = axValue(of: $0.underlyingElement, attr: kAXRoleAttribute, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else { return false }
return childRole.lowercased() == role.lowercased()
return childRole.lowercased() == role.lowercased()
}
guard index < matchingChildren.count else {
dLog("Child not found for component: \(pathComponent) at index \(index). Role: \(role). For element \(currentElementDesc). Matching children count: \(matchingChildren.count)")
return nil
@ -78,4 +78,4 @@ public func navigateToElement(from rootElement: Element, pathHint: [String], isD
}
}
return currentElement
}
}

View File

@ -2,28 +2,28 @@ import Foundation
// CustomCharacterSet struct from Scanner
public struct CustomCharacterSet {
private var characters: Set<Character>
public init(characters: Set<Character>) {
self.characters = characters
}
public init(charactersInString: String) {
self.characters = Set(charactersInString.map { $0 })
}
public func contains(_ character: Character) -> Bool {
return self.characters.contains(character)
}
public mutating func add(_ characters: Set<Character>) {
self.characters.formUnion(characters)
}
public func adding(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.union(characters))
}
public mutating func remove(_ characters: Set<Character>) {
self.characters.subtract(characters)
}
public func removing(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.subtracting(characters))
}
private var characters: Set<Character>
public init(characters: Set<Character>) {
self.characters = characters
}
public init(charactersInString: String) {
self.characters = Set(charactersInString.map { $0 })
}
public func contains(_ character: Character) -> Bool {
return self.characters.contains(character)
}
public mutating func add(_ characters: Set<Character>) {
self.characters.formUnion(characters)
}
public func adding(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.union(characters))
}
public mutating func remove(_ characters: Set<Character>) {
self.characters.subtract(characters)
}
public func removing(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.subtracting(characters))
}
// Add some common character sets that might be useful, similar to Foundation.CharacterSet
public static var whitespacesAndNewlines: CustomCharacterSet {
@ -39,4 +39,4 @@ public struct CustomCharacterSet {
public static func characters(in string: String) -> CustomCharacterSet {
return CustomCharacterSet(charactersInString: string)
}
}
}

View File

@ -13,7 +13,7 @@ public func decodeExpectedArray(fromString: String, isDebugLoggingEnabled: Bool,
// func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let trimmedString = fromString.trimmingCharacters(in: .whitespacesAndNewlines)
// Try JSON deserialization first for robustness with escaped characters, etc.
if trimmedString.hasPrefix("[") && trimmedString.hasSuffix("]") {
if let jsonData = trimmedString.data(using: .utf8) {
@ -21,7 +21,7 @@ public func decodeExpectedArray(fromString: String, isDebugLoggingEnabled: Bool,
// Attempt to decode as [String]
if let array = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String] {
return array
}
}
// Fallback: if it decodes as [Any], convert elements to String
else if let anyArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [Any] {
return anyArray.compactMap { item -> String? in
@ -39,7 +39,7 @@ public func decodeExpectedArray(fromString: String, isDebugLoggingEnabled: Bool,
}
}
}
// Fallback to comma-separated parsing if JSON fails or string isn't JSON-like
// Remove brackets first if they exist for comma parsing
var stringToSplit = trimmedString
@ -70,15 +70,15 @@ public func decodeExpectedArray(fromString: String, isDebugLoggingEnabled: Bool,
}
return stringToSplit.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
// Do not filter out empty strings if they are explicitly part of the list e.g. "a,,b"
// The original did .filter { !$0.isEmpty }, which might be too aggressive.
// For now, let's keep all components and let caller decide if empty strings are valid.
// Re-evaluating: if a component is empty after trimming, it usually means an empty element.
// Example: "[a, ,b]" -> ["a", "", "b"]. Example "a," -> ["a", ""].
// The original .filter { !$0.isEmpty } would turn "a,," into ["a"]
// Let's retain the original filtering of completely empty strings after trim,
// as "[a,,b]" usually implies "[a,b]" in lenient contexts.
// If explicit empty strings like `["a", "", "b"]` are needed, JSON is better.
.filter { !$0.isEmpty }
}
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
// Do not filter out empty strings if they are explicitly part of the list e.g. "a,,b"
// The original did .filter { !$0.isEmpty }, which might be too aggressive.
// For now, let's keep all components and let caller decide if empty strings are valid.
// Re-evaluating: if a component is empty after trimming, it usually means an empty element.
// Example: "[a, ,b]" -> ["a", "", "b"]. Example "a," -> ["a", ""].
// The original .filter { !$0.isEmpty } would turn "a,," into ["a"]
// Let's retain the original filtering of completely empty strings after trim,
// as "[a,,b]" usually implies "[a,b]" in lenient contexts.
// If explicit empty strings like `["a", "", "b"]` are needed, JSON is better.
.filter { !$0.isEmpty }
}

View File

@ -8,29 +8,29 @@ import Foundation
// Scanner class from Scanner
class Scanner {
// MARK: - Properties and Initialization
let string: String
var location: Int = 0
init(string: String) {
self.string = string
}
var isAtEnd: Bool {
return self.location >= self.string.count
}
// MARK: - Properties and Initialization
let string: String
var location: Int = 0
init(string: String) {
self.string = string
}
var isAtEnd: Bool {
return self.location >= self.string.count
}
// MARK: - Character Set Scanning
// A more conventional scanUpTo (scans until a character in the set is found)
@discardableResult func scanUpToCharacters(in charSet: CustomCharacterSet) -> String? {
let initialLocation = self.location
var scannedCharacters = String()
while self.location < self.string.count {
let currentChar = self.string[self.location]
if charSet.contains(currentChar) { break }
scannedCharacters.append(currentChar)
self.location += 1
}
return scannedCharacters.isEmpty && self.location == initialLocation ? nil : scannedCharacters
}
@ -38,12 +38,12 @@ class Scanner {
@discardableResult func scanCharacters(in charSet: CustomCharacterSet) -> String? {
let initialLocation = self.location
var characters = String()
while self.location < self.string.count, charSet.contains(self.string[self.location]) {
characters.append(self.string[self.location])
self.location += 1
}
if characters.isEmpty {
self.location = initialLocation // Revert if nothing was scanned
return nil
@ -51,131 +51,131 @@ class Scanner {
return characters
}
@discardableResult func scan(characterSet: CustomCharacterSet) -> Character? {
guard self.location < self.string.count else { return nil }
let character = self.string[self.location]
guard characterSet.contains(character) else { return nil }
self.location += 1
return character
}
@discardableResult func scan(characterSet: CustomCharacterSet) -> String? {
var characters = String()
while let character: Character = self.scan(characterSet: characterSet) {
characters.append(character)
}
return characters.isEmpty ? nil : characters
}
// MARK: - Specific Character and String Scanning
@discardableResult func scan(character: Character, options: NSString.CompareOptions = []) -> Character? {
guard self.location < self.string.count else { return nil }
let characterString = String(character)
if characterString.compare(String(self.string[self.location]), options: options, range: nil, locale: nil) == .orderedSame {
self.location += 1
return character
}
return nil
}
@discardableResult func scan(string: String, options: NSString.CompareOptions = []) -> String? {
let savepoint = self.location
var characters = String()
for character in string {
if let charScanned = self.scan(character: character, options: options) {
characters.append(charScanned)
} else {
@discardableResult func scan(characterSet: CustomCharacterSet) -> Character? {
guard self.location < self.string.count else { return nil }
let character = self.string[self.location]
guard characterSet.contains(character) else { return nil }
self.location += 1
return character
}
@discardableResult func scan(characterSet: CustomCharacterSet) -> String? {
var characters = String()
while let character: Character = self.scan(characterSet: characterSet) {
characters.append(character)
}
return characters.isEmpty ? nil : characters
}
// MARK: - Specific Character and String Scanning
@discardableResult func scan(character: Character, options: NSString.CompareOptions = []) -> Character? {
guard self.location < self.string.count else { return nil }
let characterString = String(character)
if characterString.compare(String(self.string[self.location]), options: options, range: nil, locale: nil) == .orderedSame {
self.location += 1
return character
}
return nil
}
@discardableResult func scan(string: String, options: NSString.CompareOptions = []) -> String? {
let savepoint = self.location
var characters = String()
for character in string {
if let charScanned = self.scan(character: character, options: options) {
characters.append(charScanned)
} else {
self.location = savepoint // Revert on failure
return nil
}
}
// If we scanned the whole string, it's a match.
return characters.count == string.count ? characters : { self.location = savepoint; return nil }()
}
func scan(token: String, options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(string: token, options: options)
}
func scan(strings: [String], options: NSString.CompareOptions = []) -> String? {
for stringEntry in strings {
if let scannedString = self.scan(string: stringEntry, options: options) {
return scannedString
}
}
return nil
}
func scan(tokens: [String], options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(strings: tokens, options: options)
}
// MARK: - Integer Scanning
func scanSign() -> Int? {
return self.scan(dictionary: ["+": 1, "-": -1])
}
// Private helper that scans and returns a string of digits
private func scanDigits() -> String? {
return self.scanCharacters(in: .decimalDigits)
}
// Calculate integer value from digit string with given base
private func integerValue<T: BinaryInteger>(from digitString: String, base: T = 10) -> T {
return digitString.reduce(T(0)) { result, char in
result * base + T(Int(String(char))!)
}
}
func scanUnsignedInteger<T: UnsignedInteger>() -> T? {
self.scanWhitespaces()
guard let digitString = self.scanDigits() else { return nil }
return integerValue(from: digitString)
}
func scanInteger<T: SignedInteger>() -> T? {
let savepoint = self.location
self.scanWhitespaces()
// Parse sign if present
let sign = self.scanSign() ?? 1
// Parse digits
guard let digitString = self.scanDigits() else {
// If we found a sign but no digits, revert and return nil
if sign != 1 {
self.location = savepoint
}
return nil
}
// Calculate final value with sign applied
return T(sign) * integerValue(from: digitString)
}
return nil
}
}
// If we scanned the whole string, it's a match.
return characters.count == string.count ? characters : { self.location = savepoint; return nil }()
}
func scan(token: String, options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(string: token, options: options)
}
func scan(strings: [String], options: NSString.CompareOptions = []) -> String? {
for stringEntry in strings {
if let scannedString = self.scan(string: stringEntry, options: options) {
return scannedString
}
}
return nil
}
func scan(tokens: [String], options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(strings: tokens, options: options)
}
// MARK: - Integer Scanning
func scanSign() -> Int? {
return self.scan(dictionary: ["+": 1, "-": -1])
}
// Private helper that scans and returns a string of digits
private func scanDigits() -> String? {
return self.scanCharacters(in: .decimalDigits)
}
// Calculate integer value from digit string with given base
private func integerValue<T: BinaryInteger>(from digitString: String, base: T = 10) -> T {
return digitString.reduce(T(0)) { result, char in
result * base + T(Int(String(char))!)
}
}
func scanUnsignedInteger<T: UnsignedInteger>() -> T? {
self.scanWhitespaces()
guard let digitString = self.scanDigits() else { return nil }
return integerValue(from: digitString)
}
func scanInteger<T: SignedInteger>() -> T? {
let savepoint = self.location
self.scanWhitespaces()
// Parse sign if present
let sign = self.scanSign() ?? 1
// Parse digits
guard let digitString = self.scanDigits() else {
// If we found a sign but no digits, revert and return nil
if sign != 1 {
self.location = savepoint
}
return nil
}
// Calculate final value with sign applied
return T(sign) * integerValue(from: digitString)
}
// MARK: - Floating Point Scanning
// Attempt to parse Double with a compact implementation
func scanDouble() -> Double? {
scanWhitespaces()
let initialLocation = self.location
// Parse sign
let sign: Double = (scan(character: "-") != nil) ? -1.0 : { _ = scan(character: "+"); return 1.0 }()
// Buffer to build the numeric string
var numberStr = ""
var hasDigits = false
// Parse integer part
if let digits = scanCharacters(in: .decimalDigits) {
numberStr += digits
hasDigits = true
}
// Parse fractional part
let dotLocation = location
if scan(character: ".") != nil {
@ -188,19 +188,19 @@ class Scanner {
location = dotLocation
}
}
// If no digits found in either integer or fractional part, revert and return nil
if !hasDigits {
location = initialLocation
return nil
}
// Parse exponent
var exponent = 0
let expLocation = location
if scan(character: "e", options: .caseInsensitive) != nil {
let expSign: Double = (scan(character: "-") != nil) ? -1.0 : { _ = scan(character: "+"); return 1.0 }()
if let expDigits = scanCharacters(in: .decimalDigits), let expValue = Int(expDigits) {
exponent = Int(expSign) * expValue
} else {
@ -208,7 +208,7 @@ class Scanner {
location = expLocation
}
}
// Convert to final double value
if var value = Double(numberStr) {
value *= sign
@ -217,39 +217,39 @@ class Scanner {
}
return value
}
// If conversion fails, revert everything
location = initialLocation
return nil
}
// Mapping hex characters to their integer values
private static let hexValues: [Character: Int] = [
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9,
"a": 10, "b": 11, "c": 12, "d": 13, "e": 14, "f": 15,
"A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
]
func scanHexadecimalInteger<T: UnsignedInteger>() -> T? {
let initialLoc = location
let hexCharSet = CustomCharacterSet(charactersInString: Self.characterSets.hexDigits)
var value: T = 0
var digitCount = 0
while let char: Character = scan(characterSet: hexCharSet),
let digit = Self.hexValues[char] {
value = value * 16 + T(digit)
digitCount += 1
}
if digitCount == 0 {
location = initialLoc // Revert if nothing was scanned
return nil
}
return value
}
// Mapping hex characters to their integer values
private static let hexValues: [Character: Int] = [
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9,
"a": 10, "b": 11, "c": 12, "d": 13, "e": 14, "f": 15,
"A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
]
func scanHexadecimalInteger<T: UnsignedInteger>() -> T? {
let initialLoc = location
let hexCharSet = CustomCharacterSet(charactersInString: Self.characterSets.hexDigits)
var value: T = 0
var digitCount = 0
while let char: Character = scan(characterSet: hexCharSet),
let digit = Self.hexValues[char] {
value = value * 16 + T(digit)
digitCount += 1
}
if digitCount == 0 {
location = initialLoc // Revert if nothing was scanned
return nil
}
return value
}
// Helper function for power calculation with FloatingPoint types
private func scannerPower<T: FloatingPoint>(base: T, exponent: Int) -> T {
@ -262,57 +262,57 @@ class Scanner {
return result
}
// MARK: - Identifier Scanning
// Character sets for identifier scanning
static private let characterSets = (
lowercaseLetters: "abcdefghijklmnopqrstuvwxyz",
uppercaseLetters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
hexDigits: "0123456789abcdefABCDEF"
)
static var identifierFirstCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + "_")
}
static var identifierFollowingCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + characterSets.digits + "_")
}
func scanIdentifier() -> String? {
scanWhitespaces()
let savepoint = location
// Scan first character (must be letter or underscore)
guard let firstChar: Character = scan(characterSet: Self.identifierFirstCharSet) else {
location = savepoint
return nil
}
// Begin with the first character
var identifier = String(firstChar)
// Scan remaining characters (can include digits)
while let nextChar: Character = scan(characterSet: Self.identifierFollowingCharSet) {
identifier.append(nextChar)
}
return identifier
}
// MARK: - Whitespace Scanning
func scanWhitespaces() {
_ = self.scanCharacters(in: .whitespacesAndNewlines)
}
// MARK: - Dictionary-based Scanning
func scan<T>(dictionary: [String: T], options: NSString.CompareOptions = []) -> T? {
for (key, value) in dictionary {
if self.scan(string: key, options: options) != nil {
// Original Scanner asserts string == key, which is true if scan(string:) returns non-nil.
return value
}
}
return nil
}
// MARK: - Identifier Scanning
// Character sets for identifier scanning
static private let characterSets = (
lowercaseLetters: "abcdefghijklmnopqrstuvwxyz",
uppercaseLetters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
hexDigits: "0123456789abcdefABCDEF"
)
static var identifierFirstCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + "_")
}
static var identifierFollowingCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + characterSets.digits + "_")
}
func scanIdentifier() -> String? {
scanWhitespaces()
let savepoint = location
// Scan first character (must be letter or underscore)
guard let firstChar: Character = scan(characterSet: Self.identifierFirstCharSet) else {
location = savepoint
return nil
}
// Begin with the first character
var identifier = String(firstChar)
// Scan remaining characters (can include digits)
while let nextChar: Character = scan(characterSet: Self.identifierFollowingCharSet) {
identifier.append(nextChar)
}
return identifier
}
// MARK: - Whitespace Scanning
func scanWhitespaces() {
_ = self.scanCharacters(in: .whitespacesAndNewlines)
}
// MARK: - Dictionary-based Scanning
func scan<T>(dictionary: [String: T], options: NSString.CompareOptions = []) -> T? {
for (key, value) in dictionary {
if self.scan(string: key, options: options) != nil {
// Original Scanner asserts string == key, which is true if scan(string:) returns non-nil.
return value
}
}
return nil
}
// Helper to get the remaining string
var remainingString: String {
@ -320,4 +320,4 @@ class Scanner {
let startIndex = string.index(string.startIndex, offsetBy: location)
return String(string[startIndex...])
}
}
}

View File

@ -2,30 +2,30 @@ import Foundation
// String extension from Scanner
extension String {
subscript (i: Int) -> Character {
return self[index(startIndex, offsetBy: i)]
}
func range(from range: NSRange) -> Range<String.Index>? {
return Range(range, in: self)
}
func range(from range: Range<String.Index>) -> NSRange {
return NSRange(range, in: self)
}
var firstLine: String? {
var line: String?
self.enumerateLines {
line = $0
$1 = true
}
return line
}
subscript (i: Int) -> Character {
return self[index(startIndex, offsetBy: i)]
}
func range(from range: NSRange) -> Range<String.Index>? {
return Range(range, in: self)
}
func range(from range: Range<String.Index>) -> NSRange {
return NSRange(range, in: self)
}
var firstLine: String? {
var line: String?
self.enumerateLines {
line = $0
$1 = true
}
return line
}
}
extension Optional {
var orNilString: String {
switch self {
case .some(let value): return "\(value)"
case .none: return "nil"
}
}
}
var orNilString: String {
switch self {
case .some(let value): return "\(value)"
case .none: return "nil"
}
}
}

View File

@ -14,7 +14,7 @@ public func extractTextContent(element: Element, isDebugLoggingEnabled: Bool, cu
var texts: [String] = []
let textualAttributes = [
kAXValueAttribute, kAXTitleAttribute, kAXDescriptionAttribute, kAXHelpAttribute,
kAXPlaceholderValueAttribute, kAXLabelValueAttribute, kAXRoleDescriptionAttribute,
kAXPlaceholderValueAttribute, kAXLabelValueAttribute, kAXRoleDescriptionAttribute
// Consider adding kAXStringForRangeParameterizedAttribute if dealing with large text views for performance
// kAXSelectedTextAttribute could also be relevant depending on use case
]
@ -28,7 +28,7 @@ public func extractTextContent(element: Element, isDebugLoggingEnabled: Bool, cu
currentDebugLogs.append(contentsOf: tempLogs) // Still collect logs if value was nil/empty
}
}
// Deduplicate while preserving order
var uniqueTexts: [String] = []
var seenTexts = Set<String>()
@ -39,4 +39,4 @@ public func extractTextContent(element: Element, isDebugLoggingEnabled: Bool, cu
}
}
return uniqueTexts.joined(separator: "\n")
}
}

View File

@ -2,43 +2,38 @@ import Foundation
// MARK: - Scannable Protocol
protocol Scannable {
init?(_ scanner: Scanner)
init?(_ scanner: Scanner)
}
// MARK: - Scannable Conformance
extension Int: Scannable {
init?(_ scanner: Scanner) {
if let value: Int = scanner.scanInteger() { self = value }
else { return nil }
}
init?(_ scanner: Scanner) {
if let value: Int = scanner.scanInteger() { self = value } else { return nil }
}
}
extension UInt: Scannable {
init?(_ scanner: Scanner) {
if let value: UInt = scanner.scanUnsignedInteger() { self = value }
else { return nil }
}
init?(_ scanner: Scanner) {
if let value: UInt = scanner.scanUnsignedInteger() { self = value } else { return nil }
}
}
extension Float: Scannable {
init?(_ scanner: Scanner) {
init?(_ scanner: Scanner) {
// Using the custom scanDouble and casting
if let value = scanner.scanDouble() { self = Float(value) }
else { return nil }
}
if let value = scanner.scanDouble() { self = Float(value) } else { return nil }
}
}
extension Double: Scannable {
init?(_ scanner: Scanner) {
if let value = scanner.scanDouble() { self = value }
else { return nil }
}
init?(_ scanner: Scanner) {
if let value = scanner.scanDouble() { self = value } else { return nil }
}
}
extension Bool: Scannable {
init?(_ scanner: Scanner) {
scanner.scanWhitespaces()
if let value: Bool = scanner.scan(dictionary: ["true": true, "false": false], options: [.caseInsensitive]) { self = value }
else { return nil }
}
}
init?(_ scanner: Scanner) {
scanner.scanWhitespaces()
if let value: Bool = scanner.scan(dictionary: ["true": true, "false": false], options: [.caseInsensitive]) { self = value } else { return nil }
}
}

View File

@ -52,18 +52,19 @@ public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .defau
}
case .illegal:
result = "Illegal AXValue"
default:
default:
// For boolean type (rawValue 4)
if type.rawValue == 4 {
var boolResult: DarwinBoolean = false
if AXValueGetValue(axValue, type, &boolResult) {
result = boolResult.boolValue ? "true" : "false"
if option == .verbose { result = "<Boolean: \(result)>"}
if option == .verbose {
result = "<Boolean: \(result)>"
}
}
}
// Other types: return generic description.
// Consider if other specific AXValueTypes need custom formatting.
break
}
return result
}
@ -98,7 +99,7 @@ public func formatCFTypeRef(_ cfValue: CFTypeRef?, option: ValueFormatOption = .
case CFStringGetTypeID():
return "\"\(escapeStringForDisplay(value as! String))\"" // Used helper
case CFAttributedStringGetTypeID():
return "\"\(escapeStringForDisplay((value as! NSAttributedString).string ))\"" // Used helper
return "\"\(escapeStringForDisplay((value as! NSAttributedString).string ))\"" // Used helper
case CFBooleanGetTypeID():
return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false"
case CFNumberGetTypeID():
@ -123,7 +124,7 @@ public func formatCFTypeRef(_ cfValue: CFTypeRef?, option: ValueFormatOption = .
case CFDictionaryGetTypeID():
let cfDict = value as! CFDictionary
let count = CFDictionaryGetCount(cfDict)
if option == .verbose || count <= 3 { // Show contents for small dicts or if verbose
if option == .verbose || count <= 3 { // Show contents for small dicts or if verbose
var swiftDict: [String: String] = [:]
if let nsDict = cfDict as? [String: AnyObject] {
for (key, val) in nsDict {
@ -132,7 +133,7 @@ public func formatCFTypeRef(_ cfValue: CFTypeRef?, option: ValueFormatOption = .
}
// Sort by key for consistent output
let sortedItems = swiftDict.sorted { $0.key < $1.key }
.map { "\"\(escapeStringForDisplay($0.key))\": \($0.value)" } // Used helper for key, value is already formatted
.map { "\"\(escapeStringForDisplay($0.key))\": \($0.value)" } // Used helper for key, value is already formatted
return "{\(sortedItems.joined(separator: ","))}"
} else {
return "<Dictionary (bridging failed), size \(count)>"
@ -157,18 +158,17 @@ extension Element {
if let titleStr = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !titleStr.isEmpty {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr): \"\(escapeStringForDisplay(titleStr))\">"
}
else if let identifierStr = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !identifierStr.isEmpty {
} else if let identifierStr = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !identifierStr.isEmpty {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) id: \"\(escapeStringForDisplay(identifierStr))\">"
} else if let valueAny = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), let valueStr = valueAny as? String, !valueStr.isEmpty, valueStr.count < 50 {
} else if let valueAny = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), let valueStr = valueAny as? String, !valueStr.isEmpty, valueStr.count < 50 {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) val: \"\(escapeStringForDisplay(valueStr))\">"
} else if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !descStr.isEmpty, descStr.count < 50 {
} else if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !descStr.isEmpty, descStr.count < 50 {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) desc: \"\(escapeStringForDisplay(descStr))\">"
}
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr)>"
}
}
}

View File

@ -30,50 +30,46 @@ public func axValue<T>(of element: AXUIElement, attr: String, isDebugLoggingEnab
// copyAttributeValue doesn't log, so no need to pass log params to it.
let rawCFValue = copyAttributeValue(element: element, attribute: attr)
// ValueUnwrapper.unwrap also needs to be audited for logging. For now, assume it doesn't log or its logs are separate.
let unwrappedValue = ValueUnwrapper.unwrap(rawCFValue, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
guard let value = unwrappedValue else {
// It's common for attributes to be missing or have no value.
// Only log if in debug mode and something was expected but not found,
guard let value = unwrappedValue else {
// It's common for attributes to be missing or have no value.
// Only log if in debug mode and something was expected but not found,
// or if rawCFValue was non-nil but unwrapped to nil (which ValueUnwrapper might handle).
// For now, let's not log here, as Element.swift's rawAttributeValue also has checks.
return nil
return nil
}
if T.self == String.self {
if let str = value as? String { return str as? T }
else if let attrStr = value as? NSAttributedString { return attrStr.string as? T }
if let str = value as? String { return str as? T } else if let attrStr = value as? NSAttributedString { return attrStr.string as? T }
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Bool.self {
if let boolVal = value as? Bool { return boolVal as? T }
else if let numVal = value as? NSNumber { return numVal.boolValue as? T }
if let boolVal = value as? Bool { return boolVal as? T } else if let numVal = value as? NSNumber { return numVal.boolValue as? T }
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Int.self {
if let intVal = value as? Int { return intVal as? T }
else if let numVal = value as? NSNumber { return numVal.intValue as? T }
if let intVal = value as? Int { return intVal as? T } else if let numVal = value as? NSNumber { return numVal.intValue as? T }
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Double.self {
if let doubleVal = value as? Double { return doubleVal as? T }
else if let numVal = value as? NSNumber { return numVal.doubleValue as? T }
if let doubleVal = value as? Double { return doubleVal as? T } else if let numVal = value as? NSNumber { return numVal.doubleValue as? T }
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == [AXUIElement].self {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> AXUIElement? in
guard let cfItem = item else { return nil }
guard let cfItem = item else { return nil }
// Ensure correct comparison for CFTypeRef type ID
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() { // Directly use AXUIElementGetTypeID()
return (cfItem as! AXUIElement)
@ -89,7 +85,7 @@ public func axValue<T>(of element: AXUIElement, attr: String, isDebugLoggingEnab
if T.self == [Element].self { // Assuming Element is a struct wrapping AXUIElement
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> Element? in
guard let cfItem = item else { return nil }
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() { // Check underlying type
return Element(cfItem as! AXUIElement)
}
@ -102,7 +98,7 @@ public func axValue<T>(of element: AXUIElement, attr: String, isDebugLoggingEnab
}
if T.self == [String].self {
if let stringArray = value as? [Any?] {
if let stringArray = value as? [Any?] {
let result = stringArray.compactMap { $0 as? String }
// Ensure all elements were successfully cast, otherwise it's not a homogenous [String] array
if result.count == stringArray.count { return result as? T }
@ -123,21 +119,21 @@ public func axValue<T>(of element: AXUIElement, attr: String, isDebugLoggingEnab
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == AXUIElement.self {
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
return (cfValue as! AXUIElement) as? T
}
let typeDescription = String(describing: type(of: value))
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
}
if let castedValue = value as? T {
return castedValue
}
dLog("axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. Unwrapped value was \(type(of: value)): \(value)")
return nil
}
@ -162,4 +158,4 @@ public func stringFromAXValueType(_ type: AXValueType) -> String {
}
return "Unknown AXValueType (rawValue: \(type.rawValue))"
}
}
}

View File

@ -30,23 +30,22 @@ public func getAXValueTypeForAttribute(element: Element, attributeName: String,
dLog("getAXValueTypeForAttribute: Failed to get raw attribute value for '\(attributeName)'")
return nil
}
guard CFGetTypeID(rawValue) == AXValueGetTypeID() else {
dLog("getAXValueTypeForAttribute: Attribute '\(attributeName)' is not an AXValue. TypeID: \(CFGetTypeID(rawValue))")
return nil
}
let axValue = rawValue as! AXValue
return AXValueGetType(axValue)
}
// Main function to create CFTypeRef for setting an attribute
// It determines the type of the attribute and then calls the appropriate parser.
@MainActor
public func createCFTypeRefFromString(stringValue: String, forElement element: Element, attributeName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> CFTypeRef? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
guard let currentRawValue = element.rawAttributeValue(named: attributeName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
throw AccessibilityError.attributeNotReadable("Could not read current value for attribute '\(attributeName)' to determine type.")
}
@ -66,7 +65,7 @@ public func createCFTypeRefFromString(stringValue: String, forElement element: E
if let doubleValue = Double(stringValue) {
return NSNumber(value: doubleValue) // CFNumber is toll-free bridged to NSNumber
} else if let intValue = Int(stringValue) {
return NSNumber(value: intValue)
return NSNumber(value: intValue)
} else {
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as Double or Int for CFNumber attribute '\(attributeName)'")
}
@ -87,7 +86,6 @@ public func createCFTypeRefFromString(stringValue: String, forElement element: E
throw AccessibilityError.attributeUnsupported("Setting attribute '\(attributeName)' of CFTypeID \(typeID) (\(typeDescription)) from string is not supported yet.")
}
// Parses a string into an AXValue for struct types like CGPoint, CGSize, CGRect, CFRange
@MainActor
private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValueType, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> AXValue? {
@ -105,17 +103,17 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
} else if components.count == 2, let xVal = Double(components[0]), let yVal = Double(components[1]) {
x = xVal; y = yVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let xScanned = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let yScanned = scanner.scanDouble()
if let xVal = xScanned, let yVal = yScanned {
x = xVal; y = yVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let xScanned = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let yScanned = scanner.scanDouble()
if let xVal = xScanned, let yVal = yScanned {
x = xVal; y = yVal
} else {
dLog("parseStringToAXValue: CGPoint parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGPoint. Expected format like 'x=10,y=20' or '10,20'.")
}
}
}
var point = CGPoint(x: x, y: y)
valueRef = AXValueCreate(targetAXValueType, &point)
@ -138,8 +136,8 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
if let wVal = wScanned, let hVal = hScanned {
w = wVal; h = hVal
} else {
dLog("parseStringToAXValue: CGSize parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGSize. Expected format like 'w=100,h=50' or '100,50'.")
dLog("parseStringToAXValue: CGSize parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGSize. Expected format like 'w=100,h=50' or '100,50'.")
}
}
var size = CGSize(width: w, height: h)
@ -155,8 +153,8 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
let hStr = components[3].split(separator: "=").last, let hVal = Double(hStr) {
x = xVal; y = yVal; w = wVal; h = hVal
} else if components.count == 4,
let xVal = Double(components[0]), let yVal = Double(components[1]),
let wVal = Double(components[2]), let hVal = Double(components[3]) {
let xVal = Double(components[0]), let yVal = Double(components[1]),
let wVal = Double(components[2]), let hVal = Double(components[3]) {
x = xVal; y = yVal; w = wVal; h = hVal
} else {
let scanner = Scanner(string: stringValue)
@ -203,21 +201,19 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
}
var range = CFRangeMake(loc, len)
valueRef = AXValueCreate(targetAXValueType, &range)
case .illegal:
dLog("parseStringToAXValue: Attempted to parse for .illegal AXValueType.")
throw AccessibilityError.attributeUnsupported("Cannot parse value for AXValueType .illegal")
case .axError:
dLog("parseStringToAXValue: Attempted to parse for .axError AXValueType.")
throw AccessibilityError.attributeUnsupported("Cannot set an attribute of AXValueType .axError")
case .axError:
dLog("parseStringToAXValue: Attempted to parse for .axError AXValueType.")
throw AccessibilityError.attributeUnsupported("Cannot set an attribute of AXValueType .axError")
default:
if targetAXValueType.rawValue == 4 {
if targetAXValueType.rawValue == 4 {
var boolVal: DarwinBoolean
if stringValue.lowercased() == "true" { boolVal = true }
else if stringValue.lowercased() == "false" { boolVal = false }
else {
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.")
}
@ -229,8 +225,8 @@ private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValu
}
if valueRef == nil {
dLog("parseStringToAXValue: AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
throw AccessibilityError.valueParsingFailed(details: "AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
dLog("parseStringToAXValue: AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
throw AccessibilityError.valueParsingFailed(details: "AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
}
return valueRef
}
}

View File

@ -78,10 +78,10 @@ struct ValueUnwrapper {
swiftDict[key] = unwrap(val, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) // Unwrap the value
}
} else {
// Fallback for more complex CFDictionary structures if direct bridging fails
// This part requires careful handling of CFDictionary keys and values
// For now, we'll log if direct bridging fails, as full CFDictionary iteration is complex.
dLog("ValueUnwrapper: Failed to bridge CFDictionary to [String: AnyObject]. Full CFDictionary iteration not yet implemented here.")
// Fallback for more complex CFDictionary structures if direct bridging fails
// This part requires careful handling of CFDictionary keys and values
// For now, we'll log if direct bridging fails, as full CFDictionary iteration is complex.
dLog("ValueUnwrapper: Failed to bridge CFDictionary to [String: AnyObject]. Full CFDictionary iteration not yet implemented here.")
}
return swiftDict
default:
@ -89,4 +89,4 @@ struct ValueUnwrapper {
return value // Return the original value if CFType is not handled
}
}
}
}

View File

@ -21,17 +21,17 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
var file: String?
@Argument(help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file) are used, this argument is ignored.")
var directPayload: String? = nil
var directPayload: String?
mutating func run() async throws {
var localDebugLogs: [String] = []
var localDebugLogs: [String] = []
if debug {
localDebugLogs.append("Debug logging enabled by --debug flag.")
}
var receivedJsonString: String? = nil
var receivedJsonString: String?
var inputSourceDescription: String = "Unspecified"
var detailedInputError: String? = nil
var detailedInputError: String?
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0)
let positionalPayloadProvided = directPayload != nil && !(directPayload?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
@ -42,7 +42,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
} else if stdin {
inputSourceDescription = "STDIN"
let stdInputHandle = FileHandle.standardInput
let stdinData = stdInputHandle.readDataToEndOfFile()
let stdinData = stdInputHandle.readDataToEndOfFile()
if let str = String(data: stdinData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !str.isEmpty {
receivedJsonString = str
localDebugLogs.append("Successfully read \(str.count) chars from STDIN.")
@ -61,17 +61,17 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
localDebugLogs.append("Successfully read from file: \(filePath)")
}
} catch {
detailedInputError = "Error: Failed to read from file '\(filePath)': \(error.localizedDescription)"
detailedInputError = "Error: Failed to read from file '\(filePath)': \(error.localizedDescription)"
}
if detailedInputError != nil { localDebugLogs.append(detailedInputError!) }
} else if let payload = directPayload, positionalPayloadProvided {
inputSourceDescription = "Direct Argument Payload"
receivedJsonString = payload.trimmingCharacters(in: .whitespacesAndNewlines)
localDebugLogs.append("Using direct argument payload. Length: \(receivedJsonString?.count ?? 0)")
} else if directPayload != nil && !positionalPayloadProvided {
detailedInputError = "Error: Direct argument payload was provided but was an empty string."
inputSourceDescription = detailedInputError!
localDebugLogs.append(detailedInputError!)
} else if directPayload != nil && !positionalPayloadProvided {
detailedInputError = "Error: Direct argument payload was provided but was an empty string."
inputSourceDescription = detailedInputError!
localDebugLogs.append(detailedInputError!)
} else {
detailedInputError = "No JSON input method specified or chosen method yielded no data."
inputSourceDescription = detailedInputError!
@ -96,10 +96,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
do {
let commandEnvelope = try JSONDecoder().decode(CommandEnvelope.self, from: Data(jsonToProcess.utf8))
var currentLogs = localDebugLogs
var currentLogs = localDebugLogs
currentLogs.append("Decoded CommandEnvelope. Type: \(commandEnvelope.command), ID: \(commandEnvelope.command_id)")
switch commandEnvelope.command {
@ -108,7 +108,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let messageValue = inputSourceDescription
let successMessage = prefix + messageValue
currentLogs.append(successMessage)
let details: String?
if let payloadData = jsonToProcess.data(using: .utf8),
let payload = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
@ -118,44 +118,44 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
} else {
details = nil
}
let successResponse = SimpleSuccessResponse(
command_id: commandEnvelope.command_id,
success: true, // Explicitly true
status: "pong",
status: "pong",
message: successMessage,
details: details,
debug_logs: debug ? currentLogs : nil
)
if let data = try? encoder.encode(successResponse), let str = String(data: data, encoding: .utf8) { print(str) }
case .getFocusedElement:
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
// Directly await the MainActor function. operationResult is non-optional.
let operationResult: HandlerResponse = await axInstance.handleGetFocusedElement(
for: appIdentifierForHandler,
requestedAttributes: requestedAttributesForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
// No semaphore needed
// operationResult is now non-optional, so we can use it directly.
let actualResponse = operationResult
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
@ -182,7 +182,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .getAttributes:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "getAttributes command requires a locator but none was provided"
@ -191,17 +191,17 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleGetAttributes method
let operationResult: HandlerResponse = await axInstance.handleGetAttributes(
for: appIdentifierForHandler,
@ -213,10 +213,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for getAttributes...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
@ -225,7 +225,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
@ -249,7 +249,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .query:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "query command requires a locator but none was provided"
@ -258,17 +258,17 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleQuery method
let operationResult: HandlerResponse = await axInstance.handleQuery(
for: appIdentifierForHandler,
@ -280,10 +280,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for query...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
@ -292,7 +292,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
@ -316,7 +316,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .describeElement:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "describeElement command requires a locator but none was provided"
@ -325,16 +325,16 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleDescribeElement method
let operationResult: HandlerResponse = await axInstance.handleDescribeElement(
for: appIdentifierForHandler,
@ -345,10 +345,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for describeElement...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
@ -357,7 +357,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
@ -381,7 +381,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .performAction:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "performAction command requires a locator but none was provided"
@ -397,10 +397,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let pathHintForHandler = commandEnvelope.path_hint
@ -416,10 +416,10 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for performAction...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
@ -428,7 +428,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
@ -452,7 +452,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .extractText:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "extractText command requires a locator but none was provided"
@ -512,7 +512,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .batch:
// The main commandEnvelope is for the batch itself.
// Sub-commands are now directly in commandEnvelope.sub_commands.
@ -523,7 +523,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
currentLogs.append("Processing batch command. Batch ID: \(commandEnvelope.command_id), Number of sub-commands: \(subCommands.count)")
let axInstance = AXorcist()
@ -536,7 +536,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug, // Use overall debug flag
currentDebugLogs: &handlerLogs
)
// Convert each HandlerResponse into a QueryResponse
var batchQueryResponses: [QueryResponse] = []
var overallSuccess = true
@ -548,27 +548,27 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errorMsg = "Mismatch between subCommands and batchHandlerResponses count."
currentLogs.append(errorMsg)
// Consider how to report this internal error
continue
continue
}
let subCommandEnvelope = subCommands[index]
let subQueryResponse = QueryResponse(
command_id: subCommandEnvelope.command_id, // Use sub-command's ID
success: subHandlerResponse.error == nil,
command: subCommandEnvelope.command.rawValue, // Use sub-command's type
handlerResponse: subHandlerResponse,
debug_logs: nil // Individual sub-command logs are part of HandlerResponse.
// QueryResponse's init handles this for its 'error' or 'data'.
// The overall batch debug log will be separate.
// QueryResponse's init handles this for its 'error' or 'data'.
// The overall batch debug log will be separate.
)
batchQueryResponses.append(subQueryResponse)
if subHandlerResponse.error != nil {
overallSuccess = false
}
}
let finalDebugLogsForBatch = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
let batchOperationResponse = BatchOperationResponse(
command_id: commandEnvelope.command_id, // ID of the overall batch from the main envelope
success: overallSuccess,
@ -596,7 +596,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: finalDebugLogsForBatch)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .collectAll:
let axInstance = AXorcist()
let handlerLogs = currentLogs // Changed var to let
@ -657,7 +657,7 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
default:
let errorMsg = "Unhandled command type: \(commandEnvelope.command)"
currentLogs.append(errorMsg)
@ -668,13 +668,13 @@ struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
var errorLogs = localDebugLogs
let basicErrorMessage = "JSON decoding error: \(error.localizedDescription)"
errorLogs.append(basicErrorMessage)
let detailedErrorMessage: String
if let decodingError = error as? DecodingError {
errorLogs.append("Decoding error details: \(decodingError.humanReadableDescription)")
detailedErrorMessage = "Failed to decode JSON command (DecodingError): \(decodingError.humanReadableDescription)"
errorLogs.append("Decoding error details: \(decodingError.humanReadableDescription)")
detailedErrorMessage = "Failed to decode JSON command (DecodingError): \(decodingError.humanReadableDescription)"
} else {
detailedErrorMessage = "Failed to decode JSON command: \(error.localizedDescription)"
detailedErrorMessage = "Failed to decode JSON command: \(error.localizedDescription)"
}
let errResponse = ErrorResponse(command_id: "decode_error", error: ErrorResponse.ErrorDetail(message: detailedErrorMessage), debug_logs: debug ? errorLogs : nil)
@ -727,7 +727,7 @@ struct QueryResponse: Codable {
let data: AXElementForEncoding? // Contains the AX element's data, adapted for encoding
let error: ErrorResponse.ErrorDetail?
let debug_logs: [String]?
// Custom initializer to bridge from HandlerResponse (from AXorcist module)
init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
self.command_id = command_id
@ -768,6 +768,5 @@ extension DecodingError {
}
/*
struct AXORC: ParsableCommand { ... old content ... }
*/
struct AXORC: ParsableCommand { ... old content ... }
*/

View File

@ -8,39 +8,39 @@ import Testing
private func setupTextEditAndGetInfo() async throws -> (pid: pid_t, axAppElement: AXUIElement?) {
let textEditBundleId = "com.apple.TextEdit"
var app: NSRunningApplication? = NSRunningApplication.runningApplications(withBundleIdentifier: textEditBundleId).first
if app == nil {
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: textEditBundleId) else {
throw TestError.generic("Could not find URL for TextEdit application.")
}
print("Attempting to launch TextEdit from URL: \(url.path)")
// Use the older launchApplication API which sometimes is more robust in test environments
// despite deprecation. Configure for async and no activation initially.
let configuration: [NSWorkspace.LaunchConfigurationKey: Any] = [:] // Empty config for older API
do {
app = try NSWorkspace.shared.launchApplication(at: url,
options: [.async, .withoutActivation],
configuration: configuration)
app = try NSWorkspace.shared.launchApplication(at: url,
options: [.async, .withoutActivation],
configuration: configuration)
print("launchApplication call completed. App PID if returned: \(app?.processIdentifier ?? -1)")
} catch {
throw TestError.appNotRunning("Failed to launch TextEdit using launchApplication(at:options:configuration:): \(error.localizedDescription)")
}
// Wait for the app to appear in running applications list
var launchedApp: NSRunningApplication? = nil
var launchedApp: NSRunningApplication?
for attempt in 1...10 { // Retry for up to 10 * 0.5s = 5 seconds
launchedApp = NSRunningApplication.runningApplications(withBundleIdentifier: textEditBundleId).first
if launchedApp != nil {
if launchedApp != nil {
print("TextEdit found running after launch, attempt \(attempt).")
break
}
try await Task.sleep(for: .milliseconds(500))
print("Waiting for TextEdit to appear in running list... attempt \(attempt)")
}
guard let runningAppAfterLaunch = launchedApp else {
throw TestError.appNotRunning("TextEdit did not appear in running applications list after launch attempt.")
throw TestError.appNotRunning("TextEdit did not appear in running applications list after launch attempt.")
}
app = runningAppAfterLaunch // Assign the found app
}
@ -79,7 +79,7 @@ private func setupTextEditAndGetInfo() async throws -> (pid: pid_t, axAppElement
try await Task.sleep(for: .seconds(2)) // Wait for new document window
}
}
// Re-check activation
if !runningApp.isActive {
runningApp.activate(options: [.activateAllWindows])
@ -94,7 +94,7 @@ private func setupTextEditAndGetInfo() async throws -> (pid: pid_t, axAppElement
} else {
print("AX API did not get a focused element during setup. Status: \(status.rawValue). This might be okay.")
}
return (pid, axAppElement)
}
@ -104,14 +104,14 @@ private func closeTextEdit() async {
guard let textEdit = NSRunningApplication.runningApplications(withBundleIdentifier: textEditBundleId).first else {
return // Not running
}
textEdit.terminate()
// Give it a moment to terminate gracefully
for _ in 0..<5 { // Check for up to 2.5 seconds
if textEdit.isTerminated { break }
try? await Task.sleep(for: .milliseconds(500))
}
if !textEdit.isTerminated {
textEdit.forceTerminate()
try? await Task.sleep(for: .milliseconds(500)) // Brief pause after force terminate
@ -138,10 +138,10 @@ private func runAXORCCommand(arguments: [String]) throws -> (String?, String?, I
let output = String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let errorOutput = String(data: errorData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
// Strip the AXORC_JSON_OUTPUT_PREFIX if present
let cleanOutput = stripJSONPrefix(from: output)
return (cleanOutput, errorOutput, process.terminationStatus)
}
@ -176,7 +176,7 @@ private func runAXORCCommandWithStdin(inputJSON: String, arguments: [String]) th
effectiveArguments.append("--stdin")
}
process.arguments = effectiveArguments
let outputPipe = Pipe()
let errorPipe = Pipe()
let inputPipe = Pipe()
@ -197,7 +197,7 @@ private func runAXORCCommandWithStdin(inputJSON: String, arguments: [String]) th
// Consider throwing an error or logging
print("Warning: Could not convert inputJSON to Data for STDIN.")
}
process.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
@ -205,9 +205,9 @@ private func runAXORCCommandWithStdin(inputJSON: String, arguments: [String]) th
let output = String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let errorOutput = String(data: errorData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let cleanOutput = stripJSONPrefix(from: output)
return (cleanOutput, errorOutput, process.terminationStatus)
}
@ -236,7 +236,7 @@ struct Locator: Codable {
case requireAction = "require_action"
case computed_name_contains
}
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
@ -252,9 +252,9 @@ struct CommandEnvelope: Codable {
let application: String?
let attributes: [String]?
let debug_logging: Bool?
// Use the locally defined Locator struct that mirrors AXorcist.Locator
let locator: Locator?
let locator: Locator?
let path_hint: [String]? // Changed from String? to [String]? to align with AXorcist.CommandEnvelope
let max_elements: Int?
let output_format: OutputFormat? // Use directly from AXorcist module (OutputFormat, not AXorcist.OutputFormat)
@ -264,10 +264,10 @@ struct CommandEnvelope: Codable {
let payload: [String: AnyCodable]? // Use directly from AXorcist module
let sub_commands: [CommandEnvelope]? // Recursive for batch command
init(command_id: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
init(command_id: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
debug_logging: Bool? = nil,
locator: Locator? = nil, // Use local Locator type
path_hint: [String]? = nil, // Aligned to [String]?
@ -324,9 +324,9 @@ struct ErrorResponse: Codable {
let message: String
}
let debug_logs: [String]?
// Custom init if needed, for now relying on synthesized one after struct change
init(command_id: String, success: Bool = false, error: ErrorDetail, debug_logs: [String]?) {
init(command_id: String, success: Bool = false, error: ErrorDetail, debug_logs: [String]?) {
self.command_id = command_id
self.success = success
self.error = error
@ -334,11 +334,9 @@ struct ErrorResponse: Codable {
}
}
// For AXElement.attributes which can be [String: Any]
// Using a simplified AnyCodable for testing purposes
struct AXElementData: Codable { // Renamed from AXElement to avoid conflict if AXorcist.AXElement is imported
let attributes: [String: AnyCodable]? // Dictionary of attributes using AnyCodable from AXorcist module
let path: [String]? // Optional path from root
@ -369,7 +367,6 @@ struct BatchOperationResponse: Codable {
let debug_logs: [String]?
}
// MARK: - Test Cases
@Test("Test Ping via STDIN")
@ -387,7 +384,7 @@ func testPingViaStdin() async throws {
#expect(terminationStatus == 0, "axorc command failed with status \(terminationStatus). Error: \(errorOutput ?? "N/A")")
#expect(errorOutput == nil || errorOutput!.isEmpty, "Expected no error output, but got: \(errorOutput!)")
guard let outputString = output else {
#expect(Bool(false), "Output was nil for ping via STDIN")
return
@ -421,7 +418,7 @@ func testPingViaFile() async throws {
#expect(terminationStatus == 0, "axorc command failed with status \(terminationStatus). Error: \(errorOutput ?? "N/A")")
#expect(errorOutput == nil || errorOutput!.isEmpty, "Expected no error output, but got: \(errorOutput ?? "N/A")")
guard let outputString = output else {
#expect(Bool(false), "Output was nil for ping via file")
return
@ -437,7 +434,6 @@ func testPingViaFile() async throws {
#expect(decodedResponse.details == payloadMessage)
}
@Test("Test Ping via direct positional argument")
func testPingViaDirectPayload() async throws {
let payloadMessage = "Hello from testPingViaDirectPayload"
@ -448,7 +444,7 @@ func testPingViaDirectPayload() async throws {
#expect(terminationStatus == 0, "axorc command failed with status \(terminationStatus). Error: \(errorOutput ?? "N/A")")
#expect(errorOutput == nil || errorOutput!.isEmpty, "Expected no error output, but got: \(errorOutput ?? "N/A")")
guard let outputString = output else {
#expect(Bool(false), "Output was nil for ping via direct payload")
return
@ -480,7 +476,7 @@ func testErrorMultipleInputMethods() async throws {
// axorc.swift now prints error to STDOUT and exits 0
#expect(terminationStatus == 0, "axorc command should return 0 with error on stdout. Status: \(terminationStatus). Error STDOUT: \(output ?? "nil"). Error STDERR: \(errorOutput ?? "nil")")
guard let outputString = output, !outputString.isEmpty else {
#expect(Bool(false), "Output was nil or empty for multiple input methods error test")
return
@ -495,7 +491,6 @@ func testErrorMultipleInputMethods() async throws {
#expect(errorResponse.error.message.contains("Multiple input flags specified"), "Unexpected error message: \(errorResponse.error.message)")
}
@Test("Test Error: No Input Provided for Ping")
func testErrorNoInputProvidedForPing() async throws {
// Run axorc with no input flags or direct payload
@ -532,9 +527,9 @@ func testLaunchAndQueryTextEdit() async throws {
// Prepare the JSON command for axorc
let commandId = "focused_textedit_test_\(UUID().uuidString)"
let attributesToFetch: [String] = [
ApplicationServices.kAXRoleAttribute as String,
ApplicationServices.kAXRoleDescriptionAttribute as String,
ApplicationServices.kAXValueAttribute as String,
ApplicationServices.kAXRoleAttribute as String,
ApplicationServices.kAXRoleDescriptionAttribute as String,
ApplicationServices.kAXValueAttribute as String,
"AXPlaceholderValue" // Custom attribute
]
@ -553,7 +548,7 @@ func testLaunchAndQueryTextEdit() async throws {
guard let inputJSON = String(data: inputJSONData, encoding: .utf8) else {
throw TestError.generic("Failed to encode CommandEnvelope to JSON string")
}
print("Input JSON for axorc:\n\(inputJSON)")
let (output, errorOutput, terminationStatus) = try runAXORCCommandWithStdin(inputJSON: inputJSON, arguments: ["--debug"])
@ -569,10 +564,10 @@ func testLaunchAndQueryTextEdit() async throws {
}
let decoder = JSONDecoder()
guard let responseData = outputJSONString.data(using: .utf8) else {
guard let responseData = outputJSONString.data(using: .utf8) else {
throw TestError.generic("Failed to convert axorc output string to Data for getFocusedElement. Output: \(outputJSONString)")
}
let queryResponse: QueryResponse
do {
queryResponse = try decoder.decode(QueryResponse.self, from: responseData)
@ -596,7 +591,7 @@ func testLaunchAndQueryTextEdit() async throws {
let expectedRole = ApplicationServices.kAXTextAreaRole as String
let actualRole = elementData.attributes?[ApplicationServices.kAXRoleAttribute as String]?.value as? String
#expect(actualRole == expectedRole, "Focused element role should be '\(expectedRole)'. Got: '\(actualRole ?? "nil")'. Attributes: \(elementData.attributes?.keys.map { $0 } ?? [])")
// Use ApplicationServices.kAXValueAttribute and cast to String for key
#expect(elementData.attributes?.keys.contains(ApplicationServices.kAXValueAttribute as String) == true, "Focused element attributes should contain kAXValueAttribute as it was requested.")
@ -604,7 +599,7 @@ func testLaunchAndQueryTextEdit() async throws {
print("axorc Debug Logs:")
logs.forEach { print($0) }
}
// Clean up TextEdit
await closeTextEdit() // Now async and @MainActor
}
@ -677,17 +672,17 @@ func testGetAttributesForTextEditApplication() async throws {
let attributes = queryResponse.data?.attributes
#expect(attributes?["AXRole"]?.value as? String == "AXApplication", "Application role should be AXApplication. Got: \(String(describing: attributes?["AXRole"]?.value))")
#expect(attributes?["AXTitle"]?.value as? String == "TextEdit", "Application title should be TextEdit. Got: \(String(describing: attributes?["AXTitle"]?.value))")
// AXWindows should be an array
if let windowsAttr = attributes?["AXWindows"] {
#expect(windowsAttr.value is [Any], "AXWindows should be an array. Type: \(type(of: windowsAttr.value))")
if let windowsArray = windowsAttr.value as? [AnyCodable] {
#expect(!windowsArray.isEmpty, "AXWindows array should not be empty if TextEdit has windows.")
} else if let windowsArray = windowsAttr.value as? [Any] { // More general check
#expect(!windowsArray.isEmpty, "AXWindows array should not be empty (general type check).")
#expect(!windowsArray.isEmpty, "AXWindows array should not be empty (general type check).")
}
} else {
#expect(attributes?["AXWindows"] != nil, "AXWindows attribute should be present.")
#expect(attributes?["AXWindows"] != nil, "AXWindows attribute should be present.")
}
#expect(queryResponse.debug_logs != nil, "Debug logs should be present.")
@ -768,7 +763,7 @@ func testQueryForTextEditTextArea() async throws {
let attributes = queryResponse.data?.attributes
#expect(attributes?["AXRole"]?.value as? String == textAreaRole, "Element role should be \(textAreaRole). Got: \(String(describing: attributes?["AXRole"]?.value))")
// AXValue might be an empty string if the new document is empty, which is fine.
#expect(attributes?["AXValue"]?.value is String, "AXValue should exist and be a string.")
#expect(attributes?["AXNumberOfCharacters"]?.value is Int, "AXNumberOfCharacters should exist and be an Int.")
@ -811,7 +806,7 @@ func testDescribeTextEditTextArea() async throws {
application: textEditBundleId,
// No attributes explicitly requested for describeElement
debug_logging: true,
locator: textAreaLocator
locator: textAreaLocator
)
let encoder = JSONEncoder()
@ -845,13 +840,13 @@ func testDescribeTextEditTextArea() async throws {
#expect(queryResponse.command == CommandType.describeElement.rawValue)
#expect(queryResponse.error == nil, "Error field should be nil. Got: \(queryResponse.error?.message ?? "N/A")")
#expect(queryResponse.data != nil, "Data field should not be nil.")
guard let attributes = queryResponse.data?.attributes else {
throw TestError.generic("Attributes dictionary is nil in describeElement response.")
}
#expect(attributes["AXRole"]?.value as? String == textAreaRole, "Element role should be \(textAreaRole). Got: \(String(describing: attributes["AXRole"]?.value))")
// describeElement should return many attributes. Check for a few common ones.
#expect(attributes["AXRoleDescription"]?.value is String, "AXRoleDescription should exist.")
#expect(attributes["AXEnabled"]?.value is Bool, "AXEnabled should exist.")
@ -916,7 +911,7 @@ func testPerformActionSetTextEditTextAreaValue() async throws {
#expect(exitCode == 0, "performAction axorc call failed. Error: \(errorOutput ?? "N/A")")
#expect(errorOutput == nil || errorOutput!.isEmpty, "STDERR for performAction should be empty. Got: \(errorOutput ?? "")")
guard let actionOutputString = output, !actionOutputString.isEmpty else {
throw TestError.generic("Output for performAction was nil/empty.")
}
@ -935,7 +930,7 @@ func testPerformActionSetTextEditTextAreaValue() async throws {
} catch {
throw TestError.generic("Failed to decode QueryResponse for performAction: \(error.localizedDescription). JSON: \(actionOutputString)")
}
// Brief pause for UI to update if necessary, though AXSetValue is often synchronous.
try await Task.sleep(for: .milliseconds(100))
@ -946,9 +941,9 @@ func testPerformActionSetTextEditTextAreaValue() async throws {
application: textEditBundleId,
attributes: ["AXValue"], // Only need AXValue
debug_logging: true,
locator: textAreaLocator
locator: textAreaLocator
)
jsonData = try encoder.encode(queryEnvelope)
guard let queryJsonString = String(data: jsonData, encoding: .utf8) else {
throw TestError.generic("Failed to create JSON for query (verify) command.")
@ -967,18 +962,18 @@ func testPerformActionSetTextEditTextAreaValue() async throws {
guard let queryResponseData = queryOutputString.data(using: .utf8) else {
throw TestError.generic("Could not convert query (verify) output to data. Output: \(queryOutputString)")
}
do {
let verifyResponse = try decoder.decode(QueryResponse.self, from: queryResponseData)
#expect(verifyResponse.command_id == queryCommandId)
#expect(verifyResponse.success == true, "Query (verify) command failed. Error: \(verifyResponse.error?.message ?? "N/A")")
guard let attributes = verifyResponse.data?.attributes else {
throw TestError.generic("Attributes nil in query (verify) response.")
}
let retrievedValue = attributes["AXValue"]?.value as? String
#expect(retrievedValue == textToSet, "AXValue after AXSetValue action did not match. Expected: '\(textToSet)'. Got: '\(retrievedValue ?? "nil")'")
#expect(verifyResponse.debug_logs != nil)
} catch {
throw TestError.generic("Failed to decode QueryResponse for query (verify): \(error.localizedDescription). JSON: \(queryOutputString)")
@ -1036,7 +1031,7 @@ func testExtractTextFromTextEditTextArea() async throws {
guard let actionOutputString = output, !actionOutputString.isEmpty else { throw TestError.generic("Output for performAction (set value) was nil/empty.") }
let actionResponse = try JSONDecoder().decode(QueryResponse.self, from: Data(actionOutputString.utf8))
#expect(actionResponse.success == true, "performAction (set value) was not successful. Error: \(actionResponse.error?.message ?? "N/A")")
try await Task.sleep(for: .milliseconds(100)) // Brief pause
// 2. Perform extractText command
@ -1047,7 +1042,7 @@ func testExtractTextFromTextEditTextArea() async throws {
debug_logging: true,
locator: textAreaLocator
)
jsonData = try encoder.encode(extractTextEnvelope)
guard let extractJsonString = String(data: jsonData, encoding: .utf8) else {
throw TestError.generic("Failed to create JSON for extractText command.")
@ -1066,25 +1061,25 @@ func testExtractTextFromTextEditTextArea() async throws {
guard let extractResponseData = extractOutputString.data(using: .utf8) else {
throw TestError.generic("Could not convert extractText output to data. Output: \(extractOutputString)")
}
let decoder = JSONDecoder()
do {
let extractQueryResponse = try decoder.decode(QueryResponse.self, from: extractResponseData)
#expect(extractQueryResponse.command_id == extractTextCommandId)
#expect(extractQueryResponse.success == true, "extractText command failed. Error: \(extractQueryResponse.error?.message ?? "N/A")")
#expect(extractQueryResponse.command == CommandType.extractText.rawValue)
guard let attributes = extractQueryResponse.data?.attributes else {
throw TestError.generic("Attributes nil in extractText response.")
}
// AXorcist.handleExtractText is expected to return the text.
// AXorcist.handleExtractText is expected to return the text.
// The most straightforward way for it to appear in QueryResponse is via an attribute in `data.attributes`.
// Common attribute for text content is AXValue. Let's assume extractText populates this or a specific "ExtractedText" attribute.
// For now, checking AXValue as it's the most standard for text areas.
let extractedValue = attributes["AXValue"]?.value as? String
#expect(extractedValue == textToSetAndExtract, "Extracted text did not match set text. Expected: '\(textToSetAndExtract)'. Got: '\(extractedValue ?? "nil")'")
#expect(extractQueryResponse.debug_logs != nil)
#expect(extractQueryResponse.debug_logs?.contains { $0.contains("Handling extractText command") || $0.contains("handleExtractText completed") } == true, "Debug logs should indicate extractText execution.")
@ -1215,12 +1210,12 @@ enum TestError: Error, CustomStringConvertible {
// Products directory helper (if not already present from previous steps)
var productsDirectory: URL {
#if os(macOS)
#if os(macOS)
// First, try the .xctest bundle method (works well in Xcode)
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
return bundle.bundleURL.deletingLastPathComponent()
}
// Fallback for SPM command-line tests if .xctest bundle isn't found as expected.
// This navigates up from the test file to the package root, then to .build/debug.
let currentFileURL = URL(fileURLWithPath: #filePath)
@ -1229,14 +1224,14 @@ var productsDirectory: URL {
// .deletingLastPathComponent() // Tests directory
// .deletingLastPathComponent() // AXorcist package root directory
let packageRootPath = currentFileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
// Try common build paths for SwiftPM
let buildPathsToTry = [
packageRootPath.appendingPathComponent(".build/debug"),
packageRootPath.appendingPathComponent(".build/arm64-apple-macosx/debug"),
packageRootPath.appendingPathComponent(".build/x86_64-apple-macosx/debug")
]
let fileManager = FileManager.default
for path in buildPathsToTry {
// Check if the directory exists and contains the axorc executable
@ -1246,7 +1241,7 @@ var productsDirectory: URL {
}
fatalError("couldn\'t find the products directory via Bundle or SPM fallback. Package root guessed as: \(packageRootPath.path). Searched paths: \(buildPathsToTry.map { $0.path }.joined(separator: ", "))")
#else
#else
return Bundle.main.bundleURL
#endif
}
#endif
}

View File

@ -8,4 +8,4 @@ class SimpleXCTest: XCTestCase {
func testAnotherExample() {
XCTAssertTrue(true, "Another simple assertion")
}
}
}