Improve readme, logging, linting

This commit is contained in:
Peter Steinberger 2025-05-27 12:00:52 +02:00
parent 18ef3c50d0
commit dbd7759fed
26 changed files with 1053 additions and 564 deletions

694
README.md
View File

@ -1,277 +1,537 @@
# AXorcist: The power of Swift compels your UI to obey!
# AXorcist - Advanced macOS Accessibility API Wrapper
<p align="center">
<img src="assets/logo.png" alt="AXorcist Logo">
</p>
AXorcist is a powerful Swift library and command-line tool for interacting with macOS Accessibility APIs. It provides programmatic control over UI elements in any application, making it ideal for automation, testing, and assistive technology development.
<p align="center">
<strong>Swift wrapper for macOS Accessibility—chainable, fuzzy-matched queries<br>that read, click, and inspect any UI.</strong>
</p>
## Table of Contents
---
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Element Search and Matching](#element-search-and-matching)
- [Available Commands](#available-commands)
- [Actions](#actions)
- [Notifications and Observing](#notifications-and-observing)
- [Command-Line Usage](#command-line-usage)
- [Advanced Examples](#advanced-examples)
- [Architecture](#architecture)
- [Troubleshooting](#troubleshooting)
**AXorcist** harnesses the dark arts of macOS Accessibility APIs to give you supernatural control over any application's interface. Whether you're automating workflows, testing applications, or building assistive technologies, AXorcist provides the incantations you need to make UI elements bend to your will.
## Features
## ✨ Supernatural Powers
- 🔍 **Powerful Search**: Find UI elements using multiple criteria with flexible matching
- 🎯 **Precise Navigation**: Navigate UI hierarchies with path-based locators
- 🎬 **Actions**: Perform clicks, set values, and trigger UI interactions
- 👁️ **Observation**: Monitor UI changes in real-time with notifications
- 🚀 **Batch Operations**: Execute multiple commands efficiently
- 📊 **Rich Attributes**: Access all accessibility attributes and computed properties
- 🔧 **CLI Tool**: Full command-line interface for scripting and automation
- 📝 **Comprehensive Logging**: Debug support with detailed operation logs
- **🔍 Element Summoning**: Conjure UI elements using flexible locator spells
- **📋 Attribute Divination**: Extract mystical properties from accessibility elements
- **⚡ Action Invocation**: Cast clicks, text input, and menu manipulation spells
- **🔄 Batch Sorcery**: Execute multiple enchantments efficiently in a single ritual
- **📜 Text Extraction**: Harvest textual essence from any UI element
- **🗺️ Path Navigation**: Navigate the ethereal element hierarchies with precision
- **🐛 Debug Scrying**: Comprehensive logging to troubleshoot your incantations
- **📊 JSON Grimoire**: Clean JSON input/output for seamless spell integration
## 📦 Summoning AXorcist
## Installation
### Swift Package Manager
Invoke AXorcist into your project by adding it to your `Package.swift` grimoire:
Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/steipete/AXorcist.git", from: "0.1.0")
.package(url: "https://github.com/yourusername/AXorcist.git", from: "1.0.0")
]
```
### Command Line Familiar
### Command Line Tool
Conjure the `axorc` command-line familiar:
Build and install the CLI tool:
```bash
git clone https://github.com/steipete/AXorcist.git
cd AXorcist
make build
make install
swift build -c release
cp .build/release/axorc /usr/local/bin/
```
## 🚀 First Incantations
## Quick Start
### Casting Swift Spells
### Swift API
```swift
import AXorcist
@MainActor
func example() async {
let axorcist = AXorcist()
// Configure logging for the operation (optional)
// AXorcist.GlobalAXLogger.startCollecting(enableDebugLogging: .verbose, forCommandID: "exampleFocusedElement")
// Initialize AXorcist
let axorcist = AXorcist()
// Summon the focused element
let result = await axorcist.handleGetFocusedElement(
for: "Safari",
requestedAttributes: ["AXRole", "AXTitle"]
)
if let element = result.data {
print("Behold! Element with attributes:", element.attributes)
}
// Retrieve and print logs (optional)
// let collectedLogs = await AXorcist.GlobalAXLogger.getLogsAsStrings()
// if !collectedLogs.isEmpty {
// print("\\nArcane Logs:")
// collectedLogs.forEach { print($0) }
// }
// await AXorcist.GlobalAXLogger.stopCollecting() // Clears logs and stops collection for this ID
}
```
### Command Line Rituals
```bash
# Divine the focused element in any application
echo '{"command_id": "1", "command": "getFocusedElement"}' | axorc --stdin
# Summon a specific button with precise locator magic
echo '{
"command_id": "2",
"command": "query",
"locator": {
"criteria": {
"AXRole": "AXButton",
"AXTitle": "Submit"
}
}
}' | axorc --stdin
```
## 📖 The Spell Book
### The AXorcist Entity
Your primary conduit to the accessibility realm, wielding eight powerful enchantments:
#### ⚡ Core Enchantments
- **`handleGetFocusedElement`**: Divine the currently focused UI element
- **`handleGetAttributes`**: Extract mystical properties using locator spells
- **`handleQuery`**: Summon elements matching your criteria
- **`handleDescribeElement`**: Reveal comprehensive element secrets
- **`handlePerformAction`**: Command UI elements to perform your bidding
- **`handleExtractText`**: Harvest textual essence from any element
- **`handleBatchCommands`**: Execute multiple spells in a single ritual
- **`handleCollectAll`**: Recursively gather all matching elements from the UI realm
#### 🧙‍♂️ Spell Components
All enchantments accept these mystical parameters:
- `appIdentifierOrNil`: Target application realm (bundle ID, name, or "focused")
- `locator`: Magical search criteria for element summoning
- `pathHint`: Array of breadcrumbs for UI navigation
- `maxDepth`: Maximum depth to traverse the element abyss
- `requestedAttributes`: Specific mystical properties to harvest
- `outputFormat`: Revelation format (`.smart`, `.verbose`, `.json_string`)
For debug logging, AXorcist now uses a global, MainActor-isolated logger (`AXorcist.GlobalAXLogger`).
Call `GlobalAXLogger.startCollecting(...)` before an operation and `GlobalAXLogger.stopCollecting()` / `GlobalAXLogger.getLogsAsStrings()` afterwards if you need to inspect logs programmatically.
### 🎯 Locator Spells
Locators are your targeting spells for finding UI elements in the digital realm:
```swift
let locator = Locator(
criteria: [
"AXRole": "AXButton",
"AXTitle": "Save Document"
],
requireAction: "AXPress" // Ensure the element can be commanded
// Create a query command
let query = QueryCommand(
appIdentifier: "com.apple.TextEdit",
locator: AXLocator(criteria: [
AXCriterion(attribute: "AXRole", value: "AXTextArea")
]),
attributesToReturn: ["AXValue", "AXRole"]
)
// Execute the command
let response = axorcist.runCommand(AXCommandEnvelope(
commandID: "query-1",
command: .query(query)
))
```
### 📜 Prophecy Format
All spells return visions in the form of `HandlerResponse`:
```swift
public struct HandlerResponse {
public var data: AXElement? // The summoned element's essence
public var error: String? // Curse description if spell failed
public var debug_logs: [String]? // Scrying logs and mystical insights
}
```
### 🔮 Element Essence Structure
The harvested element data contains:
```swift
public struct AXElement {
public var attributes: [String: AnyCodable] // Mystical element properties
public var path: [String] // Journey from app realm to element
}
```
## 🖥️ Command Line Grimoire
The `axorc` familiar accepts JSON incantations through multiple mystical channels:
### 📥 Invocation Methods
### Command Line
```bash
# Channel through the ethereal STDIN
echo '{"command": "ping", "command_id": "1"}' | axorc --stdin
# Find all buttons in Safari
echo '{"command": "query", "application": "com.apple.Safari", "locator": {"criteria": [{"attribute": "AXRole", "value": "AXButton"}]}}' | axorc --stdin
# Read from a spell scroll (file)
axorc --file command.json
# Direct magical utterance
axorc '{"command": "ping", "command_id": "1"}'
# Click the Back button
echo '{"command": "performAction", "application": "Safari", "locator": {"criteria": [{"attribute": "AXTitle", "value": "Back"}]}, "action": "AXPress"}' | axorc --stdin
```
### 🎭 Available Enchantments
## Element Search and Matching
- `ping`: Test the spiritual connection
- `getFocusedElement`: Divine the currently focused element
- `getAttributes`: Extract element properties using locator magic
- `query`: Summon elements matching your desires
- `describeElement`: Reveal comprehensive element mysteries
- `performAction`: Command elements to do your bidding
- `extractText`: Harvest textual essence from the digital realm
- `batch`: Execute multiple rituals in sequence
- `collectAll`: Gather all matching elements from the UI cosmos
### Matching Types
### 📋 Example Incantation
AXorcist supports multiple matching strategies:
- **`exact`** - Exact string match (default)
- **`contains`** - Case-insensitive substring match
- **`regex`** - Regular expression match
- **`containsAny`** - Matches if any comma-separated value is contained
- **`prefix`** - String starts with the expected value
- **`suffix`** - String ends with the expected value
### Searchable Attributes
#### Core Attributes
- `role` / `AXRole` - Element's role (e.g., "AXButton", "AXWindow")
- `subrole` / `AXSubrole` - Additional role information
- `identifier` / `id` / `AXIdentifier` - Developer-assigned unique ID
- `title` / `AXTitle` - Element's title
- `value` / `AXValue` - Element's value
- `description` / `AXDescription` - Detailed description
- `help` / `AXHelp` - Tooltip/help text
- `placeholder` / `AXPlaceholderValue` - Placeholder text
#### State Attributes
- `enabled` / `AXEnabled` - Is element enabled?
- `focused` / `AXFocused` - Is element focused?
- `hidden` / `AXHidden` - Is element hidden?
- `busy` / `AXElementBusy` - Is element busy?
#### Special Attributes
- `pid` - Process ID (exact match only)
- `domclasslist` / `AXDOMClassList` - Web element classes
- `domid` / `AXDOMIdentifier` - DOM element ID
- `computedname` / `name` - Computed accessible name
### Search Examples
#### Find button by exact title
```json
{
"criteria": [
{"attribute": "role", "value": "AXButton"},
{"attribute": "title", "value": "Submit"}
]
}
```
#### Find text field containing "email"
```json
{
"criteria": [
{"attribute": "role", "value": "AXTextField"},
{"attribute": "title", "value": "email", "match_type": "contains"}
]
}
```
#### Find element by multiple classes (web content)
```json
{
"criteria": [
{"attribute": "domclasslist", "value": "btn-primary", "match_type": "contains"}
]
}
```
#### Using OR logic
```json
{
"criteria": [
{"attribute": "title", "value": "Save"},
{"attribute": "title", "value": "Submit"},
{"attribute": "title", "value": "OK"}
],
"matchAll": false
}
```
### Path Navigation
Navigate through UI hierarchies with path hints:
```json
{
"command_id": "summon_back_button",
"command": "query",
"application": "Safari",
"locator": {
"criteria": {
"AXRole": "AXButton",
"AXTitle": "Back"
}
},
"attributes": ["AXRole", "AXTitle", "AXEnabled"]
"path_from_root": [
{"attribute": "role", "value": "AXWindow", "depth": 1},
{"attribute": "identifier", "value": "main-content", "depth": 3},
{"attribute": "role", "value": "AXButton"}
]
}
```
## 🔐 Mystical Permissions
Each path component supports:
- `attribute` - What to match
- `value` - Expected value
- `depth` - Max search depth for this step (default: 3)
- `match_type` - How to match (default: exact)
AXorcist requires sacred accessibility permissions to commune with the UI spirits. macOS will present you with a permission ritual upon first use, or you may grant access manually through:
## Available Commands
**System Preferences → Security & Privacy → Privacy → Accessibility**
### 1. Query
Find elements and retrieve their attributes.
*Grant AXorcist the power it needs to serve your digital dominion!*
## 🔨 Forging Your Arsenal
```bash
# Forge the mystical artifacts
make build
# Test the enchantments (requires local machine - CI spirits cannot grant accessibility permissions)
make test
# Complete ritual: build and test
make all
# Purify the workspace
make clean
```json
{
"command": "query",
"application": "com.apple.TextEdit",
"locator": {
"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]
},
"attributes": ["AXValue", "AXRole", "AXTitle"],
"maxDepthForSearch": 10
}
```
### 🌙 Testing in the Shadows
### 2. Perform Action
Execute actions on elements.
The test rituals require accessibility permissions to commune with the UI spirits—permissions that cannot be granted automatically on CI. While automated build specters can verify your Swift incantations compile correctly, the full test ceremonies must be performed on a local machine where you can manually grant the sacred accessibility permissions through System Preferences.
```json
{
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [{"attribute": "AXTitle", "value": "Back"}]
},
"action": "AXPress"
}
```
*The spirits of continuous integration watch over your builds, but only mortal hands can unlock the accessibility gates.*
### 3. Get Focused Element
Retrieve the currently focused element.
## ⚔️ Minimum Requirements
```json
{
"command": "getFocusedElement",
"application": "focused",
"attributes": ["AXRole", "AXTitle", "AXValue"]
}
```
- macOS 13.0 or later (The realm of Ventura and beyond)
- Swift 5.9 or later (Modern Swift sorcery)
- Xcode 15.0 or later (For apprentice developers)
### 4. Get Element at Point
Find element at specific screen coordinates.
## 🤝 Join the Coven
```json
{
"command": "getElementAtPoint",
"xCoordinate": 500,
"yCoordinate": 300,
"attributes": ["AXRole", "AXTitle"]
}
```
1. Fork this mystical repository
2. Create your feature branch (`git checkout -b feature/amazing-spell`)
3. Craft your enhancements
4. Add tests for new magical abilities
5. Ensure all enchantments pass (`make test`)
6. Submit a pull request to the main grimoire
### 5. Batch Commands
Execute multiple commands in sequence.
## 📜 Sacred License
```json
{
"command": "batch",
"commands": [
{
"command": "query",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]}
},
{
"command": "performAction",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"action": "AXSetValue",
"actionValue": "Hello, World!"
}
]
}
```
MIT License - see [LICENSE](LICENSE) scroll for the complete binding agreement.
### 6. Observe Notifications
Monitor UI changes in real-time.
## 🌟 Allied Magical Artifacts
```json
{
"command": "observe",
"application": "com.apple.TextEdit",
"notifications": ["AXValueChanged", "AXFocusedUIElementChanged"],
"includeDetails": true,
"watchChildren": false
}
```
- [AccessibilitySnapshot](https://github.com/cashapp/AccessibilitySnapshot) - iOS accessibility testing spells
- [Marathon](https://github.com/MarathonLabs/marathon) - Cross-platform test execution rituals
- [Hammerspoon](https://github.com/Hammerspoon/hammerspoon) - Lua-powered macOS automation sorcery
### 7. Collect All
Recursively collect all elements.
---
```json
{
"command": "collectAll",
"application": "Safari",
"attributes": ["AXRole", "AXTitle"],
"maxDepth": 5,
"filterCriteria": [{"attribute": "AXRole", "value": "AXButton"}]
}
```
<!-- Testing CI workflow -->
## Actions
<p align="center">
<em>May your UI elements bend to your will, and may your accessibility spells never fail. 🧙‍♂️✨</em>
</p>
Available actions to perform on elements:
- **AXPress** - Click/activate an element
- **AXIncrement** - Increment value (sliders, steppers)
- **AXDecrement** - Decrement value
- **AXConfirm** - Confirm action
- **AXCancel** - Cancel action
- **AXShowMenu** - Show context menu
- **AXPick** - Pick/select element
- **AXRaise** - Bring element to front
- **AXSetValue** - Set value (for text fields)
### Setting Text Values
```json
{
"command": "performAction",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"action": "AXSetValue",
"actionValue": "New text content"
}
```
## Notifications and Observing
Monitor UI changes with these notifications:
- **AXFocusedUIElementChanged** - Focus changes
- **AXValueChanged** - Value changes
- **AXUIElementDestroyed** - Element destruction
- **AXWindowCreated** - Window creation
- **AXWindowResized** - Window resizing
- **AXTitleChanged** - Title changes
- **AXSelectedTextChanged** - Text selection changes
- **AXLayoutChanged** - Layout updates
### Observer Example
```json
{
"command": "observe",
"application": "TextEdit",
"notifications": ["AXValueChanged", "AXFocusedUIElementChanged"],
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"includeDetails": true
}
```
## Command-Line Usage
### Basic Usage
```bash
# Run command from file
axorc --file command.json
# Run command from stdin
echo '{"command": "ping"}' | axorc --stdin
# Pretty print output
axorc --file command.json --pretty
# Include debug logging
axorc --file command.json --debug
```
### Advanced CLI Examples
```bash
# Find all enabled buttons
echo '{
"command": "query",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXEnabled", "value": "true"}
]
}
}' | axorc --stdin --pretty
# Click button using path navigation
echo '{
"command": "performAction",
"application": "com.apple.Safari",
"locator": {
"path_from_root": [
{"attribute": "AXRole", "value": "AXWindow"},
{"attribute": "AXIdentifier", "value": "toolbar"}
],
"criteria": [{"attribute": "AXTitle", "value": "Back"}]
},
"action": "AXPress"
}' | axorc --stdin
```
## Advanced Examples
### Complex Search with Path Navigation
```json
{
"command": "query",
"application": "com.apple.Safari",
"locator": {
"path_from_root": [
{"attribute": "AXRole", "value": "AXWindow", "depth": 1},
{"attribute": "AXRole", "value": "AXWebArea", "depth": 5}
],
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXDOMClassList", "value": "submit-button primary", "match_type": "contains"}
]
},
"attributes": ["AXTitle", "AXValue", "AXEnabled", "AXPosition", "AXSize"]
}
```
### Automated Form Filling
```json
{
"command": "batch",
"commands": [
{
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXTextField"},
{"attribute": "AXPlaceholderValue", "value": "Email", "match_type": "contains"}
]
},
"action": "AXSetValue",
"actionValue": "user@example.com"
},
{
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXTextField"},
{"attribute": "AXPlaceholderValue", "value": "Password", "match_type": "contains"}
]
},
"action": "AXSetValue",
"actionValue": "secretpassword"
},
{
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXTitle", "value": "Sign In", "match_type": "contains"}
]
},
"action": "AXPress"
}
]
}
```
### Monitoring Text Changes
```json
{
"command": "observe",
"application": "com.apple.TextEdit",
"notifications": ["AXValueChanged", "AXSelectedTextChanged"],
"locator": {
"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]
},
"includeDetails": true,
"watchChildren": true
}
```
## Architecture
### Core Components
- **AXorcist** - Main orchestrator class
- **Element** - Wrapper around AXUIElement with convenience methods
- **ElementSearch** - Tree traversal and matching engine
- **AXElementMatcher** - Criteria matching logic
- **PathNavigator** - Hierarchical navigation
- **AXObserverCenter** - Notification management
### Thread Safety
All operations are MainActor-isolated for thread safety when interacting with the Accessibility API.
### Performance Optimizations
- Early termination on first match
- Depth-limited searches
- Efficient tree traversal with visitor pattern
- Caching of frequently accessed attributes
## Troubleshooting
### Permission Issues
Ensure your app has accessibility permissions:
```json
{
"command": "isProcessTrusted"
}
```
### Finding Elements
Use the debug flag to see detailed search logs:
```bash
axorc --file command.json --debug
```
### Common Issues
1. **Element not found**: Try broader criteria or increase search depth
2. **Action failed**: Ensure element is enabled and supports the action
3. **Observer not working**: Check notification names and app identifier
### Debug Mode
Enable debug logging in commands:
```json
{
"command": "query",
"debugLogging": true,
...
}
```
## License
[Add your license information here]
## Contributing
[Add contribution guidelines here]

View File

@ -205,7 +205,16 @@ public struct ObserveCommand: Sendable {
public let includeElementDetails: [String]?
public let maxDepthForSearch: Int
public init(appIdentifier: String?, locator: Locator? = nil, notifications: [String], includeDetails: Bool = true, watchChildren: Bool = false, notificationName: AXNotification, includeElementDetails: [String]? = nil, maxDepthForSearch: Int = 10) {
public init(
appIdentifier: String?,
locator: Locator? = nil,
notifications: [String],
includeDetails: Bool = true,
watchChildren: Bool = false,
notificationName: AXNotification,
includeElementDetails: [String]? = nil,
maxDepthForSearch: Int = 10
) {
self.appIdentifier = appIdentifier
self.locator = locator
self.notifications = notifications
@ -258,7 +267,3 @@ public struct AXBatchCommand: Sendable {
self.commands = commands
}
}
// Alias for backward compatibility if needed
public typealias AXSubCommand = AXCommand
public typealias BatchCommandEnvelope = AXBatchCommand

View File

@ -0,0 +1,44 @@
// AXError+StringConversion.swift - String conversion for AXError
import ApplicationServices
extension AXError {
var stringValue: String {
switch self {
case .success:
return "success"
case .failure:
return "failure"
case .apiDisabled:
return "apiDisabled"
case .invalidUIElement:
return "invalidUIElement"
case .invalidUIElementObserver:
return "invalidUIElementObserver"
case .cannotComplete:
return "cannotComplete"
case .attributeUnsupported:
return "attributeUnsupported"
case .actionUnsupported:
return "actionUnsupported"
case .notificationUnsupported:
return "notificationUnsupported"
case .notImplemented:
return "notImplemented"
case .notificationAlreadyRegistered:
return "notificationAlreadyRegistered"
case .notificationNotRegistered:
return "notificationNotRegistered"
case .noValue:
return "noValue"
case .parameterizedAttributeUnsupported:
return "parameterizedAttributeUnsupported"
case .illegalArgument:
return "illegalArgument"
case .notEnoughPrecision:
return "notEnoughPrecision"
@unknown default:
return "unknownError (\(self.rawValue))"
}
}
}

View File

@ -146,7 +146,11 @@ public class AXObserverCenter {
// After all unsubscriptions, observers and subscriptions should be empty.
if !self.observers.isEmpty || !self.subscriptions.isEmpty || !self.subscriptionTokens.isEmpty { // Added self.
axWarningLog("removeAllObservers: observers, subscriptions, or tokens list not empty after mass unsubscribe. observers: \(self.observers.count), subscriptions: \(self.subscriptions.count), tokens: \(self.subscriptionTokens.count)") // Added self.
axWarningLog(
"removeAllObservers: observers, subscriptions, or tokens list not empty after mass unsubscribe. " +
"observers: \(self.observers.count), subscriptions: \(self.subscriptions.count), " +
"tokens: \(self.subscriptionTokens.count)"
) // Added self.
// Force clear for safety, though unsubscribe should handle it.
self.observers.removeAll() // Added self.
self.subscriptions.removeAll() // Added self.

View File

@ -8,7 +8,7 @@ extension AXorcist {
public func handlePerformAction(command: PerformActionCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandlePerformAction: App '\(String(describing: command.appIdentifier))', " +
message: "HandlePerformAction: App '\(String(describing: command.appIdentifier))', " +
"Locator: \(command.locator), Action: \(command.action), " +
"Value: \(String(describing: command.value))"
))
@ -20,20 +20,20 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandlePerformAction: Element not found for app " +
let errorMessage = error ?? "HandlePerformAction: Element not found for app " +
"'\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandlePerformAction: Found element: " +
message: "HandlePerformAction: Found element: " +
"\(element.briefDescription(option: ValueFormatOption.smart))"
))
// Check if action is supported before attempting
if !element.isActionSupported(command.action) {
let errorMessage = "AXorcist/HandlePerformAction: Action '\(command.action)' " +
let errorMessage = "HandlePerformAction: Action '\(command.action)' " +
"is NOT supported by element " +
"\(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: errorMessage))
@ -52,14 +52,14 @@ extension AXorcist {
if let actionValue = command.value?.value {
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "AXorcist/HandlePerformAction: Action value provided but not used: \(actionValue)"
message: "HandlePerformAction: Action value provided but not used: \(actionValue)"
))
}
try element.performAction(command.action)
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandlePerformAction: Successfully performed action " +
message: "HandlePerformAction: Successfully performed action " +
"'\(command.action)' on " +
"\(element.briefDescription(option: ValueFormatOption.smart))."
))
@ -67,7 +67,7 @@ extension AXorcist {
payload: AnyCodable(["message": "Action '\(command.action)' performed successfully."])
)
} catch {
let errorMessage = "AXorcist/HandlePerformAction: Failed to perform action " +
let errorMessage = "HandlePerformAction: Failed to perform action " +
"'\(command.action)' on " +
"\(element.briefDescription(option: ValueFormatOption.smart)). " +
"Error: \(error)"
@ -80,7 +80,7 @@ extension AXorcist {
public func handleSetFocusedValue(command: SetFocusedValueCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandleSetFocusedValue: App '\(String(describing: command.appIdentifier))', " +
message: "HandleSetFocusedValue: App '\(String(describing: command.appIdentifier))', " +
"Locator: \(command.locator), Value: '\(command.value)'"
))
@ -91,14 +91,14 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandleSetFocusedValue: Element not found for app " +
let errorMessage = error ?? "HandleSetFocusedValue: Element not found for app " +
"'\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleSetFocusedValue: Found element: " +
message: "HandleSetFocusedValue: Found element: " +
"\(element.briefDescription(option: ValueFormatOption.smart))"
))
@ -118,7 +118,7 @@ extension AXorcist {
if element.isActionSupported(AXActionNames.kAXPressAction) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleSetFocusedValue: Element not directly " +
message: "HandleSetFocusedValue: Element not directly " +
"focusable by kAXFocusedAttribute, but supports kAXPressAction. " +
"Attempting press."
))
@ -126,18 +126,18 @@ extension AXorcist {
try element.performAction(AXActionNames.kAXPressAction)
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleSetFocusedValue: Successfully pressed " +
message: "HandleSetFocusedValue: Successfully pressed " +
"element to potentially gain focus."
))
} catch {
let pressError = "AXorcist/HandleSetFocusedValue: Element " +
let pressError = "HandleSetFocusedValue: Element " +
"\(element.briefDescription(option: ValueFormatOption.smart)) " +
"could not be pressed to potentially gain focus."
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: pressError))
// Continue to try setting value, but log this warning.
}
} else {
let focusError = "AXorcist/HandleSetFocusedValue: Element " +
let focusError = "HandleSetFocusedValue: Element " +
"\(element.briefDescription(option: ValueFormatOption.smart)) " +
"is not focusable (kAXFocusedAttribute not settable and " +
"kAXPressAction not supported). Cannot reliably set focused value."
@ -150,14 +150,14 @@ extension AXorcist {
if isFocusable {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleSetFocusedValue: Attempting to set " +
message: "HandleSetFocusedValue: Attempting to set " +
"kAXFocusedAttribute to true for " +
"\(element.briefDescription(option: ValueFormatOption.smart))"
))
if !element.setValue(true, forAttribute: AXAttributeNames.kAXFocusedAttribute) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "AXorcist/HandleSetFocusedValue: Failed to set " +
message: "HandleSetFocusedValue: Failed to set " +
"kAXFocusedAttribute for " +
"\(element.briefDescription(option: ValueFormatOption.smart)), " +
"but proceeding to set value."
@ -172,14 +172,14 @@ extension AXorcist {
// 3. Set the value (kAXValueAttribute)
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleSetFocusedValue: Attempting to set " +
message: "HandleSetFocusedValue: Attempting to set " +
"kAXValueAttribute to '\(command.value)' for " +
"\(element.briefDescription(option: ValueFormatOption.smart))"
))
if element.setValue(command.value, forAttribute: AXAttributeNames.kAXValueAttribute) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandleSetFocusedValue: Successfully set value for " +
message: "HandleSetFocusedValue: Successfully set value for " +
"\(element.briefDescription(option: ValueFormatOption.smart))."
))
return .successResponse(
@ -188,7 +188,7 @@ extension AXorcist {
])
)
} else {
let setError = "AXorcist/HandleSetFocusedValue: Failed to set " +
let setError = "HandleSetFocusedValue: Failed to set " +
"kAXValueAttribute for " +
"\(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: setError))
@ -200,7 +200,7 @@ extension AXorcist {
public func handleExtractText(command: ExtractTextCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandleExtractText: App '\(String(describing: command.appIdentifier))', " +
message: "HandleExtractText: App '\(String(describing: command.appIdentifier))', " +
"Locator: \(command.locator), " +
"IncludeChildren: \(String(describing: command.includeChildren)), " +
"MaxDepth: \(String(describing: command.maxDepth))"
@ -213,14 +213,14 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = error ?? "AXorcist/HandleExtractText: Element not found for app " +
let errorMessage = error ?? "HandleExtractText: Element not found for app " +
"'\(String(describing: command.appIdentifier))' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleExtractText: Found element: " +
message: "HandleExtractText: Found element: " +
"\(element.briefDescription(option: ValueFormatOption.smart))"
))
@ -231,11 +231,11 @@ extension AXorcist {
) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandleExtractText: Extracted text: '\(textContent)'"
message: "HandleExtractText: Extracted text: '\(textContent)'"
))
return .successResponse(payload: AnyCodable(TextPayload(text: textContent)))
} else {
let message = "AXorcist/HandleExtractText: No text content found for " +
let message = "HandleExtractText: No text content found for " +
"element \(element.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: message))
return .successResponse(payload: AnyCodable(TextPayload(text: ""))) // Success, but no text

View File

@ -2,8 +2,8 @@ import Foundation
@MainActor
extension AXorcist {
public func handleBatchCommands(command: BatchCommandEnvelope) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleBatch: Received \(command.commands.count) sub-commands."))
public func handleBatchCommands(command: AXBatchCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "HandleBatch: Received \(command.commands.count) sub-commands."))
var results: [AXResponse] = []
var overallSuccess = true
var errorMessages: [String] = []
@ -11,7 +11,7 @@ extension AXorcist {
for (index, subCommandEnvelope) in command.commands.enumerated() {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "AXorcist/HandleBatch: Processing sub-command \(index + 1)/\(command.commands.count): " +
message: "HandleBatch: Processing sub-command \(index + 1)/\(command.commands.count): " +
"ID '\(subCommandEnvelope.commandID)', Type: \(subCommandEnvelope.command.type)"
))
@ -22,22 +22,22 @@ extension AXorcist {
overallSuccess = false
let errorDetail = response.error?.message ?? "Unknown error in sub-command \(subCommandEnvelope.commandID)"
errorMessages.append("Sub-command \(subCommandEnvelope.commandID) ('\(subCommandEnvelope.command.type)') failed: \(errorDetail)")
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "AXorcist/HandleBatch: Sub-command \(subCommandEnvelope.commandID) failed: \(errorDetail)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "HandleBatch: Sub-command \(subCommandEnvelope.commandID) failed: \(errorDetail)"))
}
}
if overallSuccess {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleBatch: All \(command.commands.count) sub-commands succeeded."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "HandleBatch: All \(command.commands.count) sub-commands succeeded."))
let successfulPayloads = results.map { $0.payload }
return .successResponse(payload: AnyCodable(BatchResponsePayload(results: successfulPayloads, errors: nil)))
} else {
let combinedErrorMessage = "AXorcist/HandleBatch: One or more sub-commands failed. Errors: \(errorMessages.joined(separator: "; "))"
let combinedErrorMessage = "HandleBatch: One or more sub-commands failed. Errors: \(errorMessages.joined(separator: "; "))"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: combinedErrorMessage))
return .errorResponse(message: combinedErrorMessage, code: .batchOperationFailed)
}
}
private func processSingleBatchCommand(_ command: AXSubCommand) -> AXResponse {
private func processSingleBatchCommand(_ command: AXCommand) -> AXResponse {
switch command {
case .query(let queryCommand):
return handleQuery(command: queryCommand, maxDepth: queryCommand.maxDepthForSearch)
@ -56,7 +56,7 @@ extension AXorcist {
case .getFocusedElement(let getFocusedElementCommand):
return handleGetFocusedElement(command: getFocusedElementCommand)
case .observe(let observeCommand):
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/BatchProc: Processing Observe command."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "BatchProc: Processing Observe command."))
return handleObserve(command: observeCommand)
case .collectAll(let collectAllCommand):
return handleCollectAll(command: collectAllCommand)

View File

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

View File

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

View File

@ -6,7 +6,7 @@ extension AXorcist {
public func handleObserve(command: ObserveCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "AXorcist/HandleObserve: App \(command.appIdentifier ?? "focused"), " +
message: "HandleObserve: App \(command.appIdentifier ?? "focused"), " +
"Notifications: \(command.notificationName.rawValue), " +
"Details: \(command.includeElementDetails?.joined(separator: ", ") ?? "none")"
))
@ -23,11 +23,11 @@ extension AXorcist {
)
guard let elementToObserve = targetElement else {
let errorMessage = error ?? "AXorcist/HandleObserve: Element to observe not found for app '\(appIdentifier)' with locator \(String(describing: locator))."
let errorMessage = error ?? "HandleObserve: Element to observe not found for app '\(appIdentifier)' with locator \(String(describing: locator))."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleObserve: Element to observe: \(elementToObserve.briefDescription(option: ValueFormatOption.smart))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "HandleObserve: Element to observe: \(elementToObserve.briefDescription(option: ValueFormatOption.smart))"))
let callback: AXObserverManager.AXNotificationCallback = { _, axUIElement, notification, userInfo in
let element = Element(axUIElement)
@ -45,18 +45,18 @@ extension AXorcist {
do {
try AXObserverManager.shared.addObserver(for: elementToObserve, notification: command.notificationName, callback: callback)
let successMessage = "AXorcist/HandleObserve: Successfully started observing '\(command.notificationName)' on \(elementToObserve.briefDescription(option: ValueFormatOption.smart))."
let successMessage = "HandleObserve: Successfully started observing '\(command.notificationName)' on \(elementToObserve.briefDescription(option: ValueFormatOption.smart))."
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: successMessage))
return .successResponse(payload: AnyCodable(["message": successMessage]))
} catch let obError as AXObserverManager.ObserverError {
let errorMessage = "AXorcist/HandleObserve: Failed to add observer. " +
let errorMessage = "HandleObserve: Failed to add observer. " +
"Error: \(obError.localizedDescription) (Code: \(obError)). " +
"Pid for element: \(elementToObserve.pid()?.description ?? "N/A") " +
"Notification: \(command.notificationName)"
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .observationFailed)
} catch {
let errorMessage = "AXorcist/HandleObserve: Failed to add observer with unknown error: " +
let errorMessage = "HandleObserve: Failed to add observer with unknown error: " +
"\(error.localizedDescription) for element " +
"\(elementToObserve.briefDescription(option: ValueFormatOption.smart)) " +
"Notification: \(command.notificationName)"

View File

@ -9,13 +9,13 @@ import Foundation
extension AXorcist {
// MARK: - Query Handler
public func handleQuery(command: QueryCommand, maxDepth externalMaxDepth: Int?) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleQuery: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "HandleQuery: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator)"))
let appIdentifier = command.appIdentifier ?? "focused"
let resolvedMaxDepth = externalMaxDepth ?? 10
// DEBUG LOG FOR MAX DEPTH
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleQuery: externalMaxDepth = \(String(describing: externalMaxDepth)), resolved maxDepth = \(resolvedMaxDepth)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "HandleQuery: externalMaxDepth = \(String(describing: externalMaxDepth)), resolved maxDepth = \(resolvedMaxDepth)"))
let (foundElement, findError) = findTargetElement(
for: appIdentifier,
@ -24,11 +24,11 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleQuery: Element not found for app '\(appIdentifier)' with locator \(command.locator)."
let errorMessage = findError ?? "HandleQuery: Element not found for app '\(appIdentifier)' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleQuery: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "HandleQuery: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
// Fetch attributes specified in command.attributesToReturn, or default if nil/empty
let attributesToFetch = command.attributesToReturn ?? AXMiscConstants.defaultAttributesToFetch
@ -39,7 +39,11 @@ extension AXorcist {
// MARK: - Get Attributes Handler
public func handleGetAttributes(command: GetAttributesCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleGetAttrs: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator), Attributes: \(command.attributes.joined(separator: ", "))"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "HandleGetAttrs: App '\(command.appIdentifier ?? "focused")', " +
"Locator: \(command.locator), Attributes: \(command.attributes.joined(separator: ", "))"
))
let (foundElement, findError) = findTargetElement(
for: command.appIdentifier ?? "focused",
@ -48,11 +52,11 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleGetAttrs: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
let errorMessage = findError ?? "HandleGetAttrs: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetAttrs: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "HandleGetAttrs: Found element: \(element.briefDescription(option: ValueFormatOption.smart))"))
var attributesDict: [String: AXValueWrapper] = [:]
for attrName in command.attributes {
@ -64,10 +68,18 @@ extension AXorcist {
}
let briefDesc = element.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleGetAttrs: Attributes for '\(briefDesc)': \(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "HandleGetAttrs: Attributes for '\(briefDesc)': " +
"\(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"
))
// Log fetched attributes for debugging purposes
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/GetAttributes: Fetched attributes for \(briefDesc): \(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "GetAttributes: Fetched attributes for \(briefDesc): " +
"\(attributesDict.mapValues { String(describing: $0.anyValue?.value) })"
))
// Construct a simple payload containing just the attributes dictionary.
// For a more structured response like AXElementData, we'd use buildQueryResponse or similar.
@ -82,7 +94,12 @@ extension AXorcist {
// MARK: - Describe Element Handler
public func handleDescribeElement(command: DescribeElementCommand) -> AXResponse {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "AXorcist/HandleDescribe: App '\(command.appIdentifier ?? "focused")', Locator: \(command.locator), Depth: \(command.depth), IncludeIgnored: \(command.includeIgnored)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "HandleDescribe: App '\(command.appIdentifier ?? "focused")', " +
"Locator: \(command.locator), Depth: \(command.depth), " +
"IncludeIgnored: \(command.includeIgnored)"
))
let (foundElement, findError) = findTargetElement(
for: command.appIdentifier ?? "focused",
@ -91,11 +108,11 @@ extension AXorcist {
)
guard let element = foundElement else {
let errorMessage = findError ?? "AXorcist/HandleDescribe: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
let errorMessage = findError ?? "HandleDescribe: Element not found for app '\(command.appIdentifier ?? "focused")' with locator \(command.locator)."
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: errorMessage))
return .errorResponse(message: errorMessage, code: .elementNotFound)
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "AXorcist/HandleDescribe: Found element: \(element.briefDescription(option: ValueFormatOption.smart)). Describing tree..."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "HandleDescribe: Found element: \(element.briefDescription(option: ValueFormatOption.smart)). Describing tree..."))
let descriptionTree = describeElementTree(element: element, depth: command.depth, includeIgnored: command.includeIgnored, currentDepth: 0)
@ -112,7 +129,7 @@ extension AXorcist {
let textualContent = extractTextFromElement(element, maxDepth: 3) // MaxDepth set to 3 for brief text
let childrenBriefs = includeChildrenBrief ? (element.children()?.map { $0.briefDescription(option: ValueFormatOption.smart) } ?? []) : nil
let fullDesc = element.briefDescription(option: .stringified) // Using .stringified for a detailed description
let pathArray = element.path()?.components // Assuming path() returns an optional Path struct with a components array
let pathArray = element.generatePathString().components(separatedBy: " -> ") // Convert path string to array
let briefDescription = element.briefDescription(option: ValueFormatOption.smart)
let role = element.role()

View File

@ -12,7 +12,7 @@ public class AXorcist {
// Central command processing function
public func runCommand(_ commandEnvelope: AXCommandEnvelope) -> AXResponse { // Removed async
logger.log(AXLogEntry(level: .info, message: "AXorcist/RunCommand: ID '\(commandEnvelope.commandID)', Type: \(commandEnvelope.command.type)")) // Removed await
logger.log(AXLogEntry(level: .info, message: "RunCommand: ID '\(commandEnvelope.commandID)', Type: \(commandEnvelope.command.type)")) // Removed await
let response: AXResponse
switch commandEnvelope.command {
@ -46,14 +46,14 @@ public class AXorcist {
// response = .errorResponse(message: errormsg, code: .unknownCommand)
}
logger.log(AXLogEntry(level: .info, message: "AXorcist/RunCommand ID '\(commandEnvelope.commandID)' completed. Status: \(response.status)")) // Removed await
logger.log(AXLogEntry(level: .info, message: "RunCommand ID '\(commandEnvelope.commandID)' completed. Status: \(response.status)")) // Removed await
return response
}
// MARK: - CollectAll Handler (New)
internal func handleCollectAll(command: CollectAllCommand) -> AXResponse {
// Placeholder implementation - replace with actual logic
logger.log(AXLogEntry(level: .info, message: "AXorcist/HandleCollectAll: Command received for app '\(command.appIdentifier ?? "nil")'. Not yet fully implemented."))
logger.log(AXLogEntry(level: .info, message: "HandleCollectAll: Command received for app '\(command.appIdentifier ?? "nil")'. Not yet fully implemented."))
// TODO: Implement actual collect all logic using command.appIdentifier, command.attributesToReturn, command.maxDepth, command.filterCriteria, command.valueFormatOption
return .errorResponse(message: "CollectAll command not yet fully implemented.", code: .unknownCommand)
}
@ -66,6 +66,6 @@ public class AXorcist {
public func clearLogs() {
GlobalAXLogger.shared.clearEntries()
logger.log(AXLogEntry(level: .info, message: "AXorcist log history cleared."))
logger.log(AXLogEntry(level: .info, message: "Log history cleared."))
}
}

View File

@ -104,7 +104,7 @@ public enum AccessibilityError: Error, CustomStringConvertible {
case let .actionFailed(action, elDesc, axErr):
var parts: [String] = ["Action '\(action)' failed."]
if let desc = elDesc { parts.append("On element: '\(desc)'.") }
if let error = axErr { parts.append("AXError: \(axErrorToString(error)).") }
if let error = axErr { parts.append("AXError: \(error.stringValue).") }
return parts.joined(separator: " ")
// Generic & System

View File

@ -1,12 +0,0 @@
// CommandModels.swift - This file has been split into multiple files for better organization
//
// The types previously in this file have been moved to:
// - ValueFormatOption.swift
// - MatchingTypes.swift (Criterion, PathStep, Locator)
// - CommandTypes.swift (CommandType, OutputFormat)
// - NotificationTypes.swift (AXNotificationName)
// - AnyCodable.swift
// - CommandEnvelope.swift
// - AXCommands.swift (AXCommand and related command structs)
//
// This file is kept empty for backward compatibility.

View File

@ -0,0 +1,75 @@
// Element+Factory.swift - Factory methods for creating Element instances
import ApplicationServices
import AppKit
// MARK: - Static Factory Methods for System-Wide and Application Elements
extension Element {
@MainActor
public static func systemWide() -> Element {
return Element(AXUIElementCreateSystemWide())
}
@MainActor
public static func application(for pid: pid_t) -> Element? {
let appElementRef = AXUIElementCreateApplication(pid)
let testElement = Element(appElementRef)
// A basic check to see if the application element is valid (e.g., by trying to get its role)
if testElement.role() != nil { // role() is synchronous
return testElement
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "Failed to create a valid application Element for PID \(pid). Role check failed."
))
return nil
}
@MainActor
public static func application(for runningApp: NSRunningApplication) -> Element? {
return application(for: runningApp.processIdentifier)
}
@MainActor
public static func focusedApplication() -> Element? {
guard let focusedApp = NSWorkspace.shared.frontmostApplication else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "No frontmost application could be determined."
))
return nil
}
return application(for: focusedApp)
}
/// Gets the element at the specified position (system-wide)
@MainActor
public static func elementAtPoint(_ point: CGPoint, pid: pid_t = 0) -> Element? {
if pid != 0 {
// Use specific application if PID is provided
guard let appElement = application(for: pid) else {
axDebugLog("Failed to get application element for PID \(pid)")
return nil
}
return AXUIElement.elementAtPosition(
in: appElement.underlyingElement,
x: Float(point.x),
y: Float(point.y)
).map(Element.init)
} else {
// System-wide element at point
var element: AXUIElement?
let error = AXUIElementCopyElementAtPosition(
AXUIElementCreateSystemWide(),
Float(point.x),
Float(point.y),
&element
)
if error == .success, let element = element {
return Element(element)
}
return nil
}
}
}

View File

@ -0,0 +1,91 @@
// Element+TypeConversion.swift - Type conversion functionality for Element
import ApplicationServices
import Foundation
extension Element {
@MainActor
func convertCFTypeToSwiftType<T>(_ cfValue: CFTypeRef, attribute: Attribute<T>) -> T? {
// Try specific type conversions first
if let converted = convertToSpecificType(cfValue, targetType: T.self) as? T {
return converted
}
// Handle Any/AnyObject types
if T.self == Any.self || T.self == AnyObject.self {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "Attribute \(attribute.rawValue): T is Any/AnyObject. Using ValueUnwrapper."
))
return ValueUnwrapper.unwrap(cfValue) as? T
}
// Try direct cast
if let directCast = cfValue as? T {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "Basic conversion succeeded with direct cast for T = \(String(describing: T.self)), " +
"Attribute: \(attribute.rawValue)."
))
return directCast
}
// Fall back to ValueUnwrapper
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "Attempting ValueUnwrapper for T = \(String(describing: T.self)), " +
"Attribute: \(attribute.rawValue)."
))
return ValueUnwrapper.unwrap(cfValue) as? T
}
private func convertToSpecificType(_ cfValue: CFTypeRef, targetType: Any.Type) -> Any? {
let cfTypeID = CFGetTypeID(cfValue)
switch targetType {
case is String.Type:
return convertToString(cfValue, cfTypeID: cfTypeID)
case is Bool.Type:
return convertToBool(cfValue, cfTypeID: cfTypeID)
case is Int.Type:
return convertToInt(cfValue, cfTypeID: cfTypeID)
case is AXUIElement.Type:
return convertToAXUIElement(cfValue, cfTypeID: cfTypeID)
default:
return nil
}
}
private func convertToString(_ cfValue: CFTypeRef, cfTypeID: CFTypeID) -> String? {
if cfTypeID == CFStringGetTypeID() {
return (cfValue as! CFString) as String
} else if cfTypeID == CFAttributedStringGetTypeID() {
return (cfValue as! NSAttributedString).string
}
return nil
}
private func convertToBool(_ cfValue: CFTypeRef, cfTypeID: CFTypeID) -> Bool? {
if cfTypeID == CFBooleanGetTypeID() {
return CFBooleanGetValue((cfValue as! CFBoolean))
}
return nil
}
private func convertToInt(_ cfValue: CFTypeRef, cfTypeID: CFTypeID) -> Int? {
if cfTypeID == CFNumberGetTypeID() {
var intValue: Int = 0
if CFNumberGetValue((cfValue as! CFNumber), .sInt64Type, &intValue) {
return intValue
}
}
return nil
}
private func convertToAXUIElement(_ cfValue: CFTypeRef, cfTypeID: CFTypeID) -> AXUIElement? {
if cfTypeID == AXUIElementGetTypeID() {
return (cfValue as! AXUIElement)
}
return nil
}
}

View File

@ -137,56 +137,10 @@ public struct Element: Equatable, Hashable {
return nil
}
// Use the type conversion functionality from Element+TypeConversion.swift
return convertCFTypeToSwiftType(unwrappedCFValue, attribute: attribute)
}
@MainActor
private func convertCFTypeToSwiftType<T>(_ cfValue: CFTypeRef, attribute: Attribute<T>) -> T? {
// Perform basic conversions
if T.self == String.self {
if CFGetTypeID(cfValue) == CFStringGetTypeID() {
return ((cfValue as! CFString) as String as? T)
} else if CFGetTypeID(cfValue) == CFAttributedStringGetTypeID() { // Handle AttributedString
return ((cfValue as! NSAttributedString).string as? T)
}
} else if T.self == Bool.self {
if CFGetTypeID(cfValue) == CFBooleanGetTypeID() {
return (CFBooleanGetValue((cfValue as! CFBoolean)) as? T)
}
} else if T.self == Int.self {
if CFGetTypeID(cfValue) == CFNumberGetTypeID() {
var intValue: Int = 0
if CFNumberGetValue((cfValue as! CFNumber), .sInt64Type, &intValue) {
return (intValue as? T)
}
}
} else if T.self == AXUIElement.self {
if CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
return cfValue as? T
}
}
if T.self == Any.self || T.self == AnyObject.self {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Attribute \\(attribute.rawValue): T is Any/AnyObject. Using ValueUnwrapper."))
return ValueUnwrapper.unwrap(cfValue) as? T
}
if let directCast = cfValue as? T {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Basic conversion succeeded with direct cast for T = \\(String(describing: T.self)), Attribute: \\(attribute.rawValue)."))
return directCast
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Attempting ValueUnwrapper for T = \\(String(describing: T.self)), Attribute: \\(attribute.rawValue)."))
if let valueFromUnwrapper = ValueUnwrapper.unwrap(cfValue) as? T {
return valueFromUnwrapper
}
let warningMessage = "Basic conversion and ValueUnwrapper FAILED for T = \\(String(describing: T.self)), "
let warningDetail = "Attribute: \\(attribute.rawValue). Value type: \\(String(describing: CFGetTypeID(cfValue)))\\"
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: warningMessage + warningDetail))
return nil
}
@MainActor
public func rawAttributeValue(named attributeName: String) -> CFTypeRef? {
var value: CFTypeRef?
@ -208,7 +162,10 @@ public struct Element: Equatable, Hashable {
var settable: DarwinBoolean = false
let error = AXUIElementIsAttributeSettable(underlyingElement, attributeName as CFString, &settable)
if error != .success {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Error checking if attribute \\(attributeName) is settable: \\(axErrorToString(error))"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "Error checking if attribute \(attributeName) is settable: \(error.stringValue)"
))
return false
}
return settable.boolValue
@ -252,6 +209,7 @@ public struct Element: Equatable, Hashable {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Parameterized attribute '\(attribute.rawValue)' value was nil after fetch."))
return nil
}
// Use the type conversion functionality from Element+TypeConversion.swift
return convertCFTypeToSwiftType(unwrappedCFValue, attribute: attribute)
}
@ -301,123 +259,35 @@ public struct Element: Equatable, Hashable {
} else {
// Attempt direct bridging for other types; may fail if not directly bridgable.
// Consider logging a warning or throwing an error for unhandled types.
let warningMsg = "Attempting to set attribute '\\(attributeName)' with potentially "
let warningDetail = "non-CF-bridgable Swift type: \\(type(of: value)). "
let warningEnd = "This might fail or lead to unexpected behavior."
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "Attempting to set attribute '\\(attributeName)' with potentially " +
"non-CF-bridgable Swift type: \\(type(of: value)). " +
"This might fail or lead to unexpected behavior."
message: warningMsg + warningDetail + warningEnd
))
cfValue = value as CFTypeRef // This can crash if 'value' is not CF-bridgable
}
let error = AXUIElementSetAttributeValue(self.underlyingElement, attributeName as CFString, cfValue)
if error == .success {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "Successfully set attribute '\\(attributeName)' to '\\(value)' on \\(self.briefDescription(option: .short))"))
let msg = "Successfully set attribute '\\(attributeName)' to '\\(value)' on "
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: msg + "\\(self.briefDescription(option: .short))"
))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .error,
message: "Failed to set attribute '\\(attributeName)' to '\\(value)' on " +
"\\(self.briefDescription(option: .short)): \\(axErrorToString(error))"
"\\(self.briefDescription(option: .short)): \\(error.stringValue)"
))
return false
}
}
}
func axErrorToString(_ error: AXError) -> String {
switch error {
case .success: return "success"
case .failure: return "failure"
case .apiDisabled: return "apiDisabled"
case .invalidUIElement: return "invalidUIElement"
case .invalidUIElementObserver: return "invalidUIElementObserver"
case .cannotComplete: return "cannotComplete"
case .attributeUnsupported: return "attributeUnsupported"
case .actionUnsupported: return "actionUnsupported"
case .notificationUnsupported: return "notificationUnsupported"
case .notImplemented: return "notImplemented"
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
case .notificationNotRegistered: return "notificationNotRegistered"
case .noValue: return "noValue"
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
default: return "unknownError (\(error.rawValue))"
}
}
// MARK: - Static Factory Methods for System-Wide and Application Elements
extension Element {
@MainActor
public static func systemWide() -> Element {
return Element(AXUIElementCreateSystemWide())
}
@MainActor
public static func application(for pid: pid_t) -> Element? {
let appElementRef = AXUIElementCreateApplication(pid)
let testElement = Element(appElementRef)
// A basic check to see if the application element is valid (e.g., by trying to get its role)
if testElement.role() != nil { // role() is synchronous
return testElement
}
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Failed to create a valid application Element for PID \(pid). Role check failed."))
return nil
}
@MainActor
public static func application(bundleIdentifier: String) -> Element? {
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "No running application found with bundle identifier: \(bundleIdentifier)"))
return nil
}
return Element.application(for: runningApp.processIdentifier) // application(for:) is synchronous
}
@MainActor
public static func focusedApplication() -> Element? {
guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Could not get frontmost application from NSWorkspace."))
return nil
}
let pid = frontmostApp.processIdentifier
if pid == -1 { // Sometimes frontmostApplication can be non-normal app (e.g. Dock when no windows are open)
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Frontmost application has PID -1, might be a system process without typical AX support."))
// Attempt to get focused element from system-wide, might give better results
let systemElement = Element.systemWide()
if let focusedElement = systemElement.focusedUIElement() { // focusedUIElement() is sync
// Try to get the app element containing this focused element
var current: Element? = focusedElement
while let parent = current?.parent() { // parent() is sync
if parent.role() == AXRoleNames.kAXApplicationRole { // role() is sync
return parent
}
if parent == systemElement { break } // Stop if we reach systemWide
current = parent
}
}
return nil // Fallback if no app found via focused element
}
return Element.application(for: pid) // application(for:) is sync
}
@MainActor
public static func elementAtPoint(_ point: CGPoint, pid: pid_t) -> Element? {
let appAXUIElement = AXUIElement.application(pid: pid)
guard let elementAXUIElement = AXUIElement.elementAtPosition(in: appAXUIElement, x: Float(point.x), y: Float(point.y)) else {
return nil
}
return Element(elementAXUIElement)
}
// MARK: - Path Generation
@MainActor
public func path() -> Path? {
let pathArray = self.generatePathArray()
return Path(components: pathArray)
}
}
// Path structure to represent element path
public struct Path {
public let components: [String]

View File

@ -102,7 +102,11 @@ internal func determineAttributesToFetch(
// Example: Fetch all attribute names if none are specified and not for a multi-default scenario
if let names = element.attributeNames() {
attributesToFetch.append(contentsOf: names)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "determineAttributesToFetch: No specific attributes requested, fetched all \(names.count) available: \(names.joined(separator: ", "))"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "determineAttributesToFetch: No specific attributes requested, " +
"fetched all \(names.count) available: \(names.joined(separator: ", "))"
))
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message:
"determineAttributesToFetch: No specific attributes requested and " +

View File

@ -25,21 +25,10 @@ public func findTargetElement(
let criteriaDebugString = locator.criteria.map { criterion in "[\(criterion.attribute):\(criterion.value), match:\(criterion.matchType?.rawValue ?? "exact")]" }.joined(separator: ", ")
// Use criteriaDebugString in the log message
var logMessage = """
FindTargetEl: START
App: '\(appIdentifier)'
MaxDepth: \(maxDepthForSearch)
"""
if !criteriaDebugString.isEmpty {
logMessage += "\n Initial Criteria: \(criteriaDebugString)"
} else {
logMessage += "\n Initial Criteria: none"
}
logMessage += "\n PathHint (count: \(locator.rootElementPathHint?.count ?? 0)):\n -> \(pathHintDebugString)"
logger.info("\(logMessage)")
logger.info("FTE: App='\(appIdentifier)' D=\(maxDepthForSearch) C=\(criteriaDebugString.isEmpty ? "none" : criteriaDebugString) PH=\(locator.rootElementPathHint?.count ?? 0)")
guard let appElement = getApplicationElement(for: appIdentifier) else {
logger.error("FindTargetEl: Could not get application element for \(appIdentifier)")
logger.error("FTE: No app element for \(appIdentifier)")
return (nil, "Application not found or not accessible: \(appIdentifier)")
}
@ -48,7 +37,7 @@ public func findTargetElement(
// 1. Navigate by pathHint if provided
if let jsonPathComponents = locator.rootElementPathHint, !jsonPathComponents.isEmpty {
logger.debug("FindTargetEl: Path hint provided with \(jsonPathComponents.count) components. Navigating path first from \(searchStartingPointDescription).")
logger.debug("FTE: PH=\(jsonPathComponents.count) from \(searchStartingPointDescription)")
// Convert [JSONPathHintComponent] to [PathStep]
let pathSteps: [PathStep] = jsonPathComponents.map { component in
@ -62,31 +51,34 @@ public func findTargetElement(
maxDepth: maxDepthForSearch, // Path navigation steps might need their own depth concept or use overall
debugSearch: locator.debugPathSearch ?? false
) {
logger.info("FindTargetEl: Path navigation successful. New search root: \(navigatedElement.briefDescription(option: ValueFormatOption.smart))")
logger.info("FTE: Path nav OK -> \(navigatedElement.briefDescription(option: ValueFormatOption.smart))")
currentSearchElement = navigatedElement
searchStartingPointDescription = "navigated path element \(currentSearchElement.briefDescription(option: ValueFormatOption.smart))"
} else {
let pathFailedError = "FindTargetEl: Path navigation failed. Could not find element at specified path hint: [\(pathHintDebugString)]"
let pathFailedError = "FTE: Path nav failed at: [\(pathHintDebugString)]"
logger.warning("\(pathFailedError)")
return (nil, pathFailedError)
}
} else {
logger.debug("FindTargetEl: No path hint provided, or path hint was empty. Searching from \(searchStartingPointDescription).")
logger.debug("FTE: No PH, search from \(searchStartingPointDescription)")
}
// 2. After path navigation (or if no path), apply final criteria from locator.criteria
// If locator.criteria is empty, it means the path navigation itself was meant to find the target.
if locator.criteria.isEmpty {
if locator.rootElementPathHint?.isEmpty ?? true {
let noCriteriaError = "FindTargetEl: No criteria provided in locator and no path hint. Cannot perform search."
let noCriteriaError = "FTE: No criteria, no path hint"
logger.error("\(noCriteriaError)")
return (nil, noCriteriaError)
}
logger.info("FindTargetEl: Path hint was used and no further criteria specified. Returning element found at path: \(currentSearchElement.briefDescription(option: .smart))")
logger.info("FTE: PH only -> \(currentSearchElement.briefDescription(option: .smart))")
return (currentSearchElement, nil)
}
logger.debug("FindTargetEl: Applying final criteria from locator (\(locator.criteria.count) criteria) starting from \(searchStartingPointDescription). MatchAll=\(locator.matchAll ?? true), MatchType=\(locator.criteria.first?.matchType?.rawValue ?? "default/exact")")
let criteriaCount = locator.criteria.count
let matchAll = locator.matchAll ?? true
let matchType = locator.criteria.first?.matchType?.rawValue ?? "default/exact"
logger.debug("FTE: Apply C=\(criteriaCount) from \(searchStartingPointDescription) MA=\(matchAll) MT=\(matchType)")
// Use matchAll and matchType from the main Locator object for these final criteria, if they exist there.
// Otherwise, SearchVisitor will use its defaults or what's on individual Criterion objects.
@ -108,7 +100,7 @@ public func findTargetElement(
return (foundMatch, nil)
} else {
let criteriaDesc = locator.criteria.map { "\($0.attribute):\($0.value)" }.joined(separator: ", ")
let finalSearchError = "FindTargetEl: No element found matching final criteria [\(criteriaDesc)] starting from \(searchStartingPointDescription)."
let finalSearchError = "FTE: Not found C=[\(criteriaDesc)] from \(searchStartingPointDescription)"
logger.warning("\(finalSearchError)")
return (nil, finalSearchError)
}
@ -124,12 +116,12 @@ public func collectAllElements(
includeIgnored: Bool = false
) -> [Element] {
let criteriaDebugString = criteria?.map { "\($0.attribute):\($0.value)(\($0.matchType?.rawValue ?? "exact"))" }.joined(separator: ", ") ?? "all"
logger.info("CollectAll: From [\(startElement.briefDescription(option: ValueFormatOption.smart))], Criteria: [\(criteriaDebugString)], MaxDepth: \(maxDepth), Ignored: \(includeIgnored)")
logger.info("CA: From [\(startElement.briefDescription(option: ValueFormatOption.smart))] C=[\(criteriaDebugString)] D=\(maxDepth) I=\(includeIgnored)")
let visitor = CollectAllVisitor(criteria: criteria, includeIgnored: includeIgnored)
traverseAndSearch(element: startElement, visitor: visitor, currentDepth: 0, maxDepth: maxDepth)
logger.info("CollectAll: Found \(visitor.collectedElements.count) elements.")
logger.info("CA: Found \(visitor.collectedElements.count)")
return visitor.collectedElements
}
@ -212,7 +204,10 @@ public class SearchVisitor: ElementVisitor {
self.matchAllCriteriaBool = matchAllCriteria // Store
self.stopAtFirstMatchInternal = stopAtFirstMatch
self.maxDepth = maxDepth
logger.debug("SearchVisitor Init: Criteria: \(criteria.map { "\($0.attribute):\($0.value)(\($0.matchType?.rawValue ?? "exact"))" }.joined(separator: ", ")), StopAtFirst: \(stopAtFirstMatchInternal), MaxDepth: \(maxDepth), MatchType: \(matchType), MatchAll: \(matchAllCriteria)")
let criteriaDesc = criteria.map { "\($0.attribute):\($0.value)(\($0.matchType?.rawValue ?? "exact"))" }.joined(separator: ", ")
logger.debug(
"SearchVisitor Init: Criteria: \(criteriaDesc), StopAtFirst: \(stopAtFirstMatchInternal), MaxDepth: \(maxDepth), MatchType: \(matchType), MatchAll: \(matchAllCriteria)"
)
}
@MainActor
@ -224,7 +219,7 @@ public class SearchVisitor: ElementVisitor {
}
let elementDesc = element.briefDescription(option: ValueFormatOption.smart)
logger.debug("SearchVisitor Visiting: [\(elementDesc)] at depth \(depth). Criteria: \(criteria.map { "\($0.attribute):\($0.value)" }.joined(separator: ", "))")
logger.debug("SV: [\(elementDesc)] @\(depth) C:\(criteria.count)")
var matches = false
if matchAllCriteriaBool {
@ -240,15 +235,15 @@ public class SearchVisitor: ElementVisitor {
}
if matches {
logger.debug("SearchVisitor MATCH: [\(elementDesc)] at depth \(depth).")
logger.debug("SV: [\(elementDesc)] @\(depth)")
foundElement = element
allFoundElements.append(element)
if stopAtFirstMatchInternal {
logger.debug("SearchVisitor: stopAtFirstMatchInternal is true. Stopping search.")
logger.debug("SV: Stop (first match)")
return .stop
}
} else {
logger.debug("SearchVisitor NO MATCH: [\(elementDesc)] at depth \(depth).")
logger.debug("SV: ✗ [\(elementDesc)] @\(depth)")
}
return .continue
}
@ -279,16 +274,16 @@ public class CollectAllVisitor: ElementVisitor {
public func visit(element: Element, depth: Int) -> TreeVisitorResult {
let elementDesc = element.briefDescription(option: ValueFormatOption.smart)
logger.debug("CollectAllVisitor Visiting: [\(elementDesc)] at depth \(depth).")
logger.debug("CAV: [\(elementDesc)] @\(depth)")
if !includeIgnored && element.isIgnored() {
logger.debug("CollectAllVisitor: Skipping ignored element [\(elementDesc)] because includeIgnored is false.")
logger.debug("CAV: Skip ignored [\(elementDesc)]")
return .skipChildren // Skip ignored elements and their children if not including ignored
}
if let criteria = criteria {
if elementMatchesAllCriteria(element: element, criteria: criteria) {
logger.debug("CollectAllVisitor: Adding [\(elementDesc)] (matched criteria).")
logger.debug("CAV: + [\(elementDesc)] (match)")
collectedElements.append(element)
} else {
logger.debug("CollectAllVisitor: [\(elementDesc)] did NOT match criteria.")

View File

@ -23,12 +23,20 @@ internal func navigateToElementByJSONPathHint(
let componentLogString = pathComponent.descriptionForLog()
currentPathSegmentForLog += " -> " + componentLogString
if pathComponent.attribute.lowercased() == "application" {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: JSON path component \(index) is 'application'. Using current element (app root) as context for next component."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/JPHN: JSON path component \(index) is 'application'. " +
"Using current element (app root) as context for next component."
))
continue
}
if index >= overallMaxDepth {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Navigation aborted: JSON path hint index \(index) reached overallMaxDepth \(overallMaxDepth). Path so far: \(currentPathSegmentForLog)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/JPHN: Navigation aborted: JSON path hint index \(index) " +
"reached overallMaxDepth \(overallMaxDepth). Path so far: \(currentPathSegmentForLog)"
))
return nil
}
@ -44,7 +52,12 @@ internal func navigateToElementByJSONPathHint(
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JPHN: Navigation successful. Final element: [\(currentElement.briefDescription(option: ValueFormatOption.smart))]. Full path: \(currentPathSegmentForLog)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PathNav/JPHN: Navigation successful. " +
"Final element: [\(currentElement.briefDescription(option: ValueFormatOption.smart))]. " +
"Full path: \(currentPathSegmentForLog)"
))
return currentElement
}
@ -56,13 +69,21 @@ private func processJSONPathComponent(
componentLogString: String
) -> Element? {
let currentElementDescForLog = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Processing JSON path component '\(componentLogString)' at element [\(currentElementDescForLog)]. Path: \(currentPathSegmentForLog)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/JPHN: Processing JSON path component '\(componentLogString)' " +
"at element [\(currentElementDescForLog)]. Path: \(currentPathSegmentForLog)"
))
let criteriaToMatch = convertJSONPathComponentToCriteria(pathComponent)
let actualMatchType = pathComponent.matchType ?? .exact
let actualMaxDepthForSearch = pathComponent.depth ?? 1
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: Converted JSON component to criteria: \(criteriaToMatch). MatchType: \(actualMatchType.rawValue), MaxDepthForSearch: \(actualMaxDepthForSearch)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/JPHN: Converted JSON component to criteria: \(criteriaToMatch). " +
"MatchType: \(actualMatchType.rawValue), MaxDepthForSearch: \(actualMaxDepthForSearch)"
))
if actualMaxDepthForSearch > 1 {
if let deepMatch = findMatchRecursively(
@ -72,7 +93,11 @@ private func processJSONPathComponent(
maxDepth: actualMaxDepthForSearch,
pathComponentForLog: componentLogString
) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/JPHN: Deep match found for component '\(componentLogString)': [\(deepMatch.briefDescription(option: ValueFormatOption.smart))]"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PathNav/JPHN: Deep match found for component '\(componentLogString)': " +
"[\(deepMatch.briefDescription(option: ValueFormatOption.smart))]"
))
return deepMatch
}
} else {
@ -85,13 +110,27 @@ private func processJSONPathComponent(
return directChild
}
if elementMatchesAllCriteriaJSON(currentElement, criteria: criteriaToMatch, matchType: actualMatchType, forPathComponent: componentLogString) {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/JPHN: JSON path component '\(componentLogString)' matches current element [\(currentElementDescForLog)]."))
if elementMatchesAllCriteriaJSON(
currentElement,
criteria: criteriaToMatch,
matchType: actualMatchType,
forPathComponent: componentLogString
) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/JPHN: JSON path component '\(componentLogString)' " +
"matches current element [\(currentElementDescForLog)]."
))
return currentElement
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/JPHN: JSON path component '\(componentLogString)' with criteria \(criteriaToMatch) did not match any child or current element [\(currentElementDescForLog)]. Path so far: \(currentPathSegmentForLog). Search depth was \(actualMaxDepthForSearch)."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "PathNav/JPHN: JSON path component '\(componentLogString)' with criteria \(criteriaToMatch) " +
"did not match any child or current element [\(currentElementDescForLog)]. " +
"Path so far: \(currentPathSegmentForLog). Search depth was \(actualMaxDepthForSearch)."
))
return nil
}
@ -113,8 +152,18 @@ private func findMatchingChildJSON(
}
for (childIndex, child) in children.enumerated() {
if elementMatchesAllCriteriaJSON(child, criteria: criteriaToMatch, matchType: matchType, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMCJ: Found matching child at index \(childIndex) for JSON component [\(pathComponentForLog)]: [\(child.briefDescription(option: ValueFormatOption.smart))]."))
if elementMatchesAllCriteriaJSON(
child,
criteria: criteriaToMatch,
matchType: matchType,
forPathComponent: pathComponentForLog
) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PathNav/FMCJ: Found matching child at index \(childIndex) " +
"for JSON component [\(pathComponentForLog)]: " +
"[\(child.briefDescription(option: ValueFormatOption.smart))]."
))
return child
}
}
@ -158,7 +207,11 @@ private func findMatchRecursively(
maxDepth: Int,
pathComponentForLog: String
) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMR: Starting recursive search for component '\(pathComponentForLog)' with maxDepth \(maxDepth) from [\(rootElement.briefDescription(option: ValueFormatOption.smart))]"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PathNav/FMR: Starting recursive search for component '\(pathComponentForLog)' " +
"with maxDepth \(maxDepth) from [\(rootElement.briefDescription(option: ValueFormatOption.smart))]"
))
var queue: [(element: Element, depth: Int)] = [(rootElement, 0)]
var visited = Set<Element>()
@ -171,8 +224,17 @@ private func findMatchRecursively(
}
visited.insert(currentElement)
if elementMatchesAllCriteriaJSON(currentElement, criteria: criteria, matchType: matchType, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMR: Found match at depth \(currentDepth): [\(currentElement.briefDescription(option: ValueFormatOption.smart))]"))
if elementMatchesAllCriteriaJSON(
currentElement,
criteria: criteria,
matchType: matchType,
forPathComponent: pathComponentForLog
) {
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PathNav/FMR: Found match at depth \(currentDepth): " +
"[\(currentElement.briefDescription(option: ValueFormatOption.smart))]"
))
return currentElement
}

View File

@ -13,31 +13,44 @@ internal func elementMatchesAllCriteria(
forPathComponent pathComponentForLog: String // For logging
) -> Bool {
let elementDescriptionForLog = element.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_START: Checking element [\(elementDescriptionForLog)] for component [\(pathComponentForLog)]. Criteria: \(criteria)"))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PN/EMAC_START: Checking element [\(elementDescriptionForLog)] for component [\(pathComponentForLog)]. Criteria: \(criteria)"))
if criteria.isEmpty {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Criteria empty for component [\(pathComponentForLog)]. Element [\(elementDescriptionForLog)] considered a match by default."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED (empty criteria) for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/EMAC: Criteria empty for component [\(pathComponentForLog)]. " +
"Element [\(elementDescriptionForLog)] considered a match by default."
))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PN/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED (empty criteria) for component [\(pathComponentForLog)]."))
return true
}
for (key, expectedValue) in criteria {
let matchTypeForKey: JSONPathHintComponent.MatchType = (key.lowercased() == AXAttributeNames.kAXDOMClassListAttribute.lowercased()) ? .contains : .exact
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC_CRITERION: Checking criterion '\(key): \(expectedValue)' (matchType: \(matchTypeForKey.rawValue)) on element [\(elementDescriptionForLog)] for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/EMAC_CRITERION: Checking criterion '\(key): \(expectedValue)' " +
"(matchType: \(matchTypeForKey.rawValue)) on element [\(elementDescriptionForLog)] " +
"for component [\(pathComponentForLog)]."
))
let criterionDidMatch = matchSingleCriterion(element: element, key: key, expectedValue: expectedValue, matchType: matchTypeForKey, elementDescriptionForLog: elementDescriptionForLog)
let message = "PathNav/EMAC_CRITERION_RESULT: Criterion '\(key): \(expectedValue)' on [\(elementDescriptionForLog)] for [\(pathComponentForLog)]: \(criterionDidMatch ? "MATCHED" : "FAILED")"
let message = "PN/EMAC_CRITERION_RESULT: Criterion '\(key): \(expectedValue)' on [\(elementDescriptionForLog)] for [\(pathComponentForLog)]: \(criterionDidMatch ? "MATCHED" : "FAILED")"
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: message))
if !criterionDidMatch {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\(elementDescriptionForLog)] FAILED to match criterion '\(key): \(expectedValue)' for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] FAILED for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/EMAC: Element [\(elementDescriptionForLog)] FAILED to match criterion '\(key): \(expectedValue)' " +
"for component [\(pathComponentForLog)]."
))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PN/EMAC_END: Element [\(elementDescriptionForLog)] FAILED for component [\(pathComponentForLog)]."))
return false
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/EMAC: Element [\(elementDescriptionForLog)] successfully MATCHED ALL criteria for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED ALL criteria for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PN/EMAC: Element [\(elementDescriptionForLog)] successfully MATCHED ALL criteria for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PN/EMAC_END: Element [\(elementDescriptionForLog)] MATCHED ALL criteria for component [\(pathComponentForLog)]."))
return true
}
@ -51,16 +64,24 @@ internal func findMatchingChild(
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: Searching for matching child among \(children.count) children of [\(parentElement.briefDescription(option: ValueFormatOption.smart))] for component [\(pathComponentForLog)]."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/FMC: Searching for matching child among \(children.count) children of " +
"[\(parentElement.briefDescription(option: ValueFormatOption.smart))] for component [\(pathComponentForLog)]."
))
for (childIndex, child) in children.enumerated() {
if elementMatchesAllCriteria(child, criteria: criteriaToMatch, forPathComponent: pathComponentForLog) {
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/FMC: Found matching child at index \(childIndex) for component [\(pathComponentForLog)]: [\(child.briefDescription(option: ValueFormatOption.smart))]."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PN/FMC: Found matching child at index \(childIndex) for component [\(pathComponentForLog)]: " +
"[\(child.briefDescription(option: ValueFormatOption.smart))]."
))
return child
}
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/FMC: No matching child found for component [\(pathComponentForLog)] among \(children.count) children."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PN/FMC: No matching child found for component [\(pathComponentForLog)] among \(children.count) children."))
return nil
}
@ -72,5 +93,9 @@ internal func logNoMatchFound(
currentPathSegmentForLog: String
) {
let currentElementDescForLog = currentElement.briefDescription(option: ValueFormatOption.smart)
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "Path component '\(pathComponentString)' with criteria \(criteriaToMatch) did not match any child or current element [\(currentElementDescForLog)]. Path so far: \(currentPathSegmentForLog)"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .warning,
message: "Path component '\(pathComponentString)' with criteria \(criteriaToMatch) did not match any child " +
"or current element [\(currentElementDescForLog)]. Path so far: \(currentPathSegmentForLog)"
))
}

View File

@ -12,17 +12,21 @@ private let logger = Logger(label: "AXorcist.PathNavigationUtilities")
@MainActor
public func getApplicationElement(for bundleIdentifier: String) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/AppEl: Attempting to get application element for bundle identifier '\(bundleIdentifier)'."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PN/AppEl: Attempting to get application element for bundle identifier '\(bundleIdentifier)'."))
guard let runningApp = NSWorkspace.shared.runningApplications.first(where: {
$0.bundleIdentifier == bundleIdentifier
}) else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/AppEl: Could not find running application with bundle identifier '\(bundleIdentifier)'."))
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PN/AppEl: Could not find running application with bundle identifier '\(bundleIdentifier)'."))
return nil
}
let pid = runningApp.processIdentifier
let appElement = Element(AXUIElementCreateApplication(pid))
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for '\(bundleIdentifier)' (PID: \(pid)): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PN/AppEl: Obtained application element for '\(bundleIdentifier)' (PID: \(pid)): " +
"[\(appElement.briefDescription(option: ValueFormatOption.smart))]"
))
return appElement
}
@ -35,7 +39,11 @@ public func getApplicationElement(for processId: pid_t) -> Element? {
} else {
bundleIdMessagePart = ""
}
GlobalAXLogger.shared.log(AXLogEntry(level: .info, message: "PathNav/AppEl: Obtained application element for PID \(processId)\(bundleIdMessagePart): [\(appElement.briefDescription(option: ValueFormatOption.smart))]"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "PN/AppEl: Obtained application element for PID \(processId)\(bundleIdMessagePart): " +
"[\(appElement.briefDescription(option: ValueFormatOption.smart))]"
))
return appElement
}
@ -47,7 +55,7 @@ public func getElement(
pathHint: [Any],
maxDepth: Int = AXMiscConstants.defaultMaxDepthSearch
) -> Element? {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Attempting to get element for app '\(appIdentifier)' with path hint (count: \(pathHint.count))."))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PN/GetEl: Attempting to get element for app '\(appIdentifier)' with path hint (count: \(pathHint.count))."))
let startElement: Element?
if let pid = pid_t(appIdentifier) {
@ -57,21 +65,33 @@ public func getElement(
}
guard let rootElement = startElement else {
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PathNav/GetEl: Could not get root application element for '\(appIdentifier)'."))
GlobalAXLogger.shared.log(AXLogEntry(level: .warning, message: "PN/GetEl: Could not get root application element for '\(appIdentifier)'."))
return nil
}
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Root element for '\(appIdentifier)' is [\(rootElement.briefDescription(option: ValueFormatOption.smart))]. Processing path hint."))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/GetEl: Root element for '\(appIdentifier)' is " +
"[\(rootElement.briefDescription(option: ValueFormatOption.smart))]. Processing path hint."
))
if let stringPathHint = pathHint as? [String] {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Interpreting path hint as [String]. Count: \(stringPathHint.count). Hint: \(stringPathHint.joined(separator: " -> "))"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/GetEl: Interpreting path hint as [String]. Count: \(stringPathHint.count). " +
"Hint: \(stringPathHint.joined(separator: " -> "))"
))
return navigateToElement(from: rootElement, pathHint: stringPathHint, maxDepth: maxDepth)
} else if let jsonPathHint = pathHint as? [JSONPathHintComponent] {
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "PathNav/GetEl: Interpreting path hint as [JSONPathHintComponent]. Count: \(jsonPathHint.count). Hint: \(jsonPathHint.map { $0.descriptionForLog() }.joined(separator: " -> "))"))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "PN/GetEl: Interpreting path hint as [JSONPathHintComponent]. Count: \(jsonPathHint.count). " +
"Hint: \(jsonPathHint.map { $0.descriptionForLog() }.joined(separator: " -> "))"
))
let initialLogSegment = rootElement.role() == AXRoleNames.kAXApplicationRole ? "Application" : rootElement.briefDescription(option: ValueFormatOption.smart)
return navigateToElementByJSONPathHint(from: rootElement, jsonPathHint: jsonPathHint, overallMaxDepth: maxDepth, initialPathSegmentForLog: initialLogSegment)
} else {
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "PathNav/GetEl: Path hint type is not [String] or [JSONPathHintComponent]. Hint: \(pathHint). Cannot navigate."))
GlobalAXLogger.shared.log(AXLogEntry(level: .error, message: "PN/GetEl: Path hint type is not [String] or [JSONPathHintComponent]. Hint: \(pathHint). Cannot navigate."))
return nil
}
}
@ -86,10 +106,10 @@ func findDescendantAtPath(
debugSearch: Bool
) -> Element? {
var currentElement = currentRoot
logger.debug("PathNav/findDescendantAtPath: Starting path navigation. Initial root: \(currentElement.briefDescription(option: .smart)). Path components: \(pathComponents.count)")
logger.debug("PN/findDescendantAtPath: Starting path navigation. Initial root: \(currentElement.briefDescription(option: .smart)). Path components: \(pathComponents.count)")
for (pathComponentIndex, component) in pathComponents.enumerated() {
logger.debug("PathNav/findDescendantAtPath: Processing component. Current: \(currentElement.briefDescription(option: .smart))")
logger.debug("PN/findDescendantAtPath: Processing component. Current: \(currentElement.briefDescription(option: .smart))")
let searchVisitor = SearchVisitor(
criteria: component.criteria,
@ -100,20 +120,29 @@ func findDescendantAtPath(
)
// Children of the current element are where we search for the next path component
logger.debug("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Current element for child search: \(currentElement.briefDescription(option: .smart))")
logger.debug("PN/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Current element for child search: \(currentElement.briefDescription(option: .smart))")
guard let childrenToSearch = currentElement.children(strict: false), !childrenToSearch.isEmpty else {
logger.warning("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] No children found (or list was empty) for \(currentElement.briefDescription(option: .smart)). Path navigation cannot proceed further down this branch.")
let componentNum = pathComponentIndex + 1
let elementDesc = currentElement.briefDescription(option: .smart)
logger.warning(
"PN/findDescendantAtPath: [Component \(componentNum)] No children found (or list was empty) for \(elementDesc). Path navigation cannot proceed further down this branch."
)
return nil
}
logger.debug("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Found \(childrenToSearch.count) children to search.")
logger.debug("PN/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Found \(childrenToSearch.count) children to search.")
var foundMatchForThisComponent: Element?
for child in childrenToSearch {
searchVisitor.reset()
traverseAndSearch(element: child, visitor: searchVisitor, currentDepth: 0, maxDepth: component.maxDepthForStep ?? 1)
if let foundUnwrapped = searchVisitor.foundElement {
logger.info("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] MATCHED component criteria \(component.descriptionForLog()) on child: \(foundUnwrapped.briefDescription(option: ValueFormatOption.smart))")
let componentNum = pathComponentIndex + 1
let componentDesc = component.descriptionForLog()
let childDesc = foundUnwrapped.briefDescription(option: ValueFormatOption.smart)
logger.info(
"PN/findDescendantAtPath: [Component \(componentNum)] MATCHED component criteria \(componentDesc) on child: \(childDesc)"
)
foundMatchForThisComponent = foundUnwrapped
break
}
@ -121,12 +150,17 @@ func findDescendantAtPath(
if let nextElement = foundMatchForThisComponent {
currentElement = nextElement
logger.debug("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Advancing to next element: \(currentElement.briefDescription(option: .smart))")
logger.debug("PN/findDescendantAtPath: [Component \(pathComponentIndex + 1)] Advancing to next element: \(currentElement.briefDescription(option: .smart))")
} else {
logger.warning("PathNav/findDescendantAtPath: [Component \(pathComponentIndex + 1)] FAILED to find match for component criteria: \(component.descriptionForLog()) within children of \(currentElement.briefDescription(option: .smart))")
let componentNum = pathComponentIndex + 1
let componentDesc = component.descriptionForLog()
let elementDesc = currentElement.briefDescription(option: .smart)
logger.warning(
"PN/findDescendantAtPath: [Component \(componentNum)] FAILED to find match for component criteria: \(componentDesc) within children of \(elementDesc)"
)
return nil
}
}
logger.info("PathNav/findDescendantAtPath: Successfully navigated full path. Final element: \(currentElement.briefDescription(option: .smart))")
logger.info("PN/findDescendantAtPath: Successfully navigated full path. Final element: \(currentElement.briefDescription(option: .smart))")
return currentElement
}

View File

@ -132,7 +132,6 @@ public func elementMatchesAnyCriterion(
return false
}
// Overload for backward compatibility with dictionary
@MainActor
public func elementMatchesCriteria(
_ element: Element,
@ -157,7 +156,7 @@ internal func matchSingleCriterion(
) -> Bool {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC: Matching key '\(key)' (expected: '\(expectedValue)', " +
message: "SC/MSC: Matching key '\(key)' (expected: '\(expectedValue)', " +
"type: \(matchType.rawValue)) on \(elementDescriptionForLog)"
))
@ -171,7 +170,7 @@ internal func matchSingleCriterion(
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC: Key '\(key)', Expected='\(expectedValue)', MatchType='\(matchType.rawValue)', " +
message: "SC/MSC: Key '\(key)', Expected='\(expectedValue)', MatchType='\(matchType.rawValue)', " +
"Result=\(comparisonResult) on \(elementDescriptionForLog)."
))
return comparisonResult
@ -223,12 +222,12 @@ private func matchAttributeByKey(
@MainActor
private func matchRoleAttribute(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, elementDescriptionForLog: String) -> Bool {
let actual = element.role()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Role: Actual='\(actual ?? "nil")'"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SC/MSC/Role: Actual='\(actual ?? "nil")'"))
if actual == AXRoleNames.kAXTextAreaRole {
let domClassList = element.attribute(Attribute<Any>(AXAttributeNames.kAXDOMClassListAttribute))
GlobalAXLogger.shared.log(AXLogEntry(
level: .info,
message: "SearchCrit/MSC/Role: ELEMENT IS AXTextArea. " +
message: "SC/MSC/Role: ELEMENT IS AXTextArea. " +
"Its AXDOMClassList is: \(String(describing: domClassList))"
))
}
@ -243,7 +242,7 @@ private func matchRoleAttribute(element: Element, expectedValue: String, matchTy
@MainActor
private func matchSubroleAttribute(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, elementDescriptionForLog: String) -> Bool {
let actual = element.subrole()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/Subrole: Actual='\(actual ?? "nil")'"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SC/MSC/Subrole: Actual='\(actual ?? "nil")'"))
return compareStrings(
actual, expectedValue, matchType,
caseSensitive: false,
@ -255,7 +254,7 @@ private func matchSubroleAttribute(element: Element, expectedValue: String, matc
@MainActor
private func matchIdentifierAttribute(element: Element, expectedValue: String, matchType: JSONPathHintComponent.MatchType, elementDescriptionForLog: String) -> Bool {
let actual = element.identifier()
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SearchCrit/MSC/ID: Actual='\(actual ?? "nil")'"))
GlobalAXLogger.shared.log(AXLogEntry(level: .debug, message: "SC/MSC/ID: Actual='\(actual ?? "nil")'"))
return compareStrings(
actual, expectedValue, matchType,
caseSensitive: true,
@ -269,7 +268,7 @@ private func matchDomClassListAttribute(element: Element, expectedValue: String,
let actualRaw = element.attribute(Attribute<Any>(AXAttributeNames.kAXDOMClassListAttribute))
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC/DOMClassList: ActualRaw='\(String(describing: actualRaw))'"
message: "SC/MSC/DOMClassList: ActualRaw='\(String(describing: actualRaw))'"
))
return matchDomClassListCriterion(
element: element,
@ -284,7 +283,7 @@ private func matchGenericAttribute(element: Element, key: String, expectedValue:
guard let actualValueAny: Any = element.attribute(Attribute(key)) else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC/Default: Attribute '\(key)' not found or nil on " +
message: "SC/MSC/Default: Attribute '\(key)' not found or nil on " +
"\(elementDescriptionForLog). No match."
))
return false
@ -296,14 +295,14 @@ private func matchGenericAttribute(element: Element, key: String, expectedValue:
actualValueString = "\(actualValueAny)"
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC/Default: Attribute '\(key)' on \(elementDescriptionForLog) " +
message: "SC/MSC/Default: Attribute '\(key)' on \(elementDescriptionForLog) " +
"was not String (type: \(type(of: actualValueAny))), " +
"using string description: '\(actualValueString)' for comparison."
))
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/MSC/Default: Attribute '\(key)', Actual='\(actualValueString)'"
message: "SC/MSC/Default: Attribute '\(key)', Actual='\(actualValueString)'"
))
return compareStrings(
actualValueString, expectedValue, matchType,
@ -322,21 +321,21 @@ private func matchPidCriterion(element: Element, expectedValue: String, elementD
guard let actualPidT = element.pid() else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) failed to provide PID. No match."
message: "SC/PID: \(elementDescriptionForLog) (app role) failed to provide PID. No match."
))
return false
}
if String(actualPidT) == expectedPid {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) PID \(actualPidT) " +
message: "SC/PID: \(elementDescriptionForLog) (app role) PID \(actualPidT) " +
"MATCHES expected \(expectedPid)."
))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) (app role) PID \(actualPidT) " +
message: "SC/PID: \(elementDescriptionForLog) (app role) PID \(actualPidT) " +
"MISMATCHES expected \(expectedPid)."
))
return false
@ -345,7 +344,7 @@ private func matchPidCriterion(element: Element, expectedValue: String, elementD
guard let actualPidT = element.pid() else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) failed to provide PID. No match."
message: "SC/PID: \(elementDescriptionForLog) failed to provide PID. No match."
))
return false
}
@ -353,14 +352,14 @@ private func matchPidCriterion(element: Element, expectedValue: String, elementD
if actualPidString == expectedPid {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) PID \(actualPidString) " +
message: "SC/PID: \(elementDescriptionForLog) PID \(actualPidString) " +
"MATCHES expected \(expectedPid)."
))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/PID: \(elementDescriptionForLog) PID \(actualPidString) " +
message: "SC/PID: \(elementDescriptionForLog) PID \(actualPidString) " +
"MISMATCHES expected \(expectedPid)."
))
return false
@ -374,14 +373,14 @@ private func matchIsIgnoredCriterion(element: Element, expectedValue: String, el
if actualIsIgnored == expectedBool {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') " +
message: "SC/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') " +
"MATCHES expected ('\(expectedBool)')."
))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') " +
message: "SC/IsIgnored: \(elementDescriptionForLog) actual ('\(actualIsIgnored)') " +
"MISMATCHES expected ('\(expectedBool)')."
))
return false
@ -398,7 +397,7 @@ private func matchDomClassListCriterion(
guard let domClassListValue: Any = element.attribute(Attribute(AXAttributeNames.kAXDOMClassListAttribute)) else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) attribute was nil. No match."
message: "SC/DOMClass: \(elementDescriptionForLog) attribute was nil. No match."
))
return false
}
@ -423,7 +422,7 @@ private func matchDomClassListCriterion(
case .regex:
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: Regex matching for array of classes. " +
message: "SC/DOMClass: Regex matching for array of classes. " +
"Element: \(elementDescriptionForLog) Expected: \(expectedValue)."
))
matchFound = classListArray.contains { item in
@ -435,7 +434,7 @@ private func matchDomClassListCriterion(
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) (Array: \(classListArray)) " +
message: "SC/DOMClass: \(elementDescriptionForLog) (Array: \(classListArray)) " +
"match type '\(matchType.rawValue)' with '\(expectedValue)' resolved to \(matchFound)."
))
} else if let classListString = domClassListValue as? String {
@ -458,7 +457,7 @@ private func matchDomClassListCriterion(
case .regex:
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: Regex matching for space-separated class string. " +
message: "SC/DOMClass: Regex matching for space-separated class string. " +
"Element: \(elementDescriptionForLog) Expected: \(expectedValue)."
))
matchFound = classes.contains { item in
@ -470,13 +469,13 @@ private func matchDomClassListCriterion(
}
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) (String: '\(classListString)') " +
message: "SC/DOMClass: \(elementDescriptionForLog) (String: '\(classListString)') " +
"match type '\(matchType.rawValue)' with '\(expectedValue)' resolved to \(matchFound)."
))
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) attribute was not [String] or String " +
message: "SC/DOMClass: \(elementDescriptionForLog) attribute was not [String] or String " +
"(type: \(type(of: domClassListValue))). No match."
))
return false
@ -485,13 +484,13 @@ private func matchDomClassListCriterion(
if matchFound {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) MATCHED expected '\(expectedValue)' " +
message: "SC/DOMClass: \(elementDescriptionForLog) MATCHED expected '\(expectedValue)' " +
"with type '\(matchType.rawValue)'. Classes: '\(domClassListValue)'"
))
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/DOMClass: \(elementDescriptionForLog) MISMATCHED expected '\(expectedValue)' " +
message: "SC/DOMClass: \(elementDescriptionForLog) MISMATCHED expected '\(expectedValue)' " +
"with type '\(matchType.rawValue)'. Classes: '\(domClassListValue)'"
))
}
@ -544,14 +543,14 @@ public func compareStrings(
if isEmptyMatch {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/Compare: '\(attributeName)' on \(elementDescriptionForLog): " +
message: "SC/Compare: '\(attributeName)' on \(elementDescriptionForLog): " +
"Actual is nil/empty, Expected is empty. MATCHED with type '\(matchType.rawValue)'."
))
return true
} else {
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) " +
message: "SC/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) " +
"(actual: nil/empty, expected: '\(expectedValue)', type: \(matchType.rawValue)) -> MISMATCH"
))
return false
@ -588,7 +587,7 @@ public func compareStrings(
let matchStatus = result ? "MATCH" : "MISMATCH"
GlobalAXLogger.shared.log(AXLogEntry(
level: .debug,
message: "SearchCrit/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) " +
message: "SC/Compare: Attribute '\(attributeName)' on \(elementDescriptionForLog) " +
"(actual: '\(actualValue)', expected: '\(expectedValue)', type: \(matchType.rawValue), " +
"caseSensitive: \(caseSensitive)) -> \(matchStatus)"
))

View File

@ -5,7 +5,7 @@ import CoreGraphics // For CGPoint, CGSize etc.
import Foundation
// ValueFormatOption is now from ModelEnums.swift
// Assumes stringFromAXValueType is available from ValueHelpers.swift and axErrorToString is available from ErrorUtils.swift
// Assumes stringFromAXValueType is available from ValueHelpers.swift
@MainActor
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .smart) -> String {
@ -98,12 +98,10 @@ private func formatCFRangeAXValue(_ axValue: AXValue, option: ValueFormatOption)
private func formatAXErrorAXValue(_ axValue: AXValue, option: ValueFormatOption) -> String {
var error = AXError.success
if AXValueGetValue(axValue, .axError, &error) {
let result = axErrorToString(error) // Assumes axErrorToString is available
let result = error.stringValue
return option == .raw ? result : "<AXError: \(result)>"
}
return "AXValue (\(stringFromAXValueType(.axError)))"
}
// stringFromAXValueType is available from ValueHelpers.swift
// axErrorToString is available from ErrorUtils.swift

View File

@ -93,7 +93,13 @@ struct CommandExecutor {
guard let axCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert CollectAll to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for CollectAll")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: errorResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axCommand))
let handlerResponse: HandlerResponse
@ -102,7 +108,13 @@ struct CommandExecutor {
} else {
handlerResponse = HandlerResponse(data: nil, error: axResponse.error?.message ?? "CollectAll failed")
}
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: handlerResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: handlerResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
@MainActor

View File

@ -85,7 +85,13 @@ internal func handleObserveCommand(command: CommandEnvelope, axorcist: AXorcist,
guard let axObserveCommand = command.command.toAXCommand(commandEnvelope: command) else {
axErrorLog("Failed to convert Observe to AXCommand")
let errorResponse = HandlerResponse(data: nil, error: "Internal error: Failed to create AXCommand for Observe")
return finalizeAndEncodeResponse(commandId: command.commandId, commandType: command.command.rawValue, handlerResponse: errorResponse, debugCLI: debugCLI, commandDebugLogging: command.debugLogging)
return finalizeAndEncodeResponse(
commandId: command.commandId,
commandType: command.command.rawValue,
handlerResponse: errorResponse,
debugCLI: debugCLI,
commandDebugLogging: command.debugLogging
)
}
let axResponse = axorcist.runCommand(AXCommandEnvelope(commandID: command.commandId, command: axObserveCommand))

View File

@ -112,7 +112,7 @@ extension CommandType {
}
let axSubCommands = batchSubCommands.compactMap { subCmdEnv -> AXBatchCommand.SubCommandEnvelope? in
guard let axSubCmd = subCmdEnv.command.toAXCommand(commandEnvelope: subCmdEnv) else {
axErrorLog("toAXCommand: Failed to convert subCommand '\(subCmdEnv.commandId)' of type '\(subCmdEnv.command.rawValue)' to AXSubCommand.")
axErrorLog("toAXCommand: Failed to convert subCommand '\(subCmdEnv.commandId)' of type '\(subCmdEnv.command.rawValue)' to AXCommand.")
return nil
}
return AXBatchCommand.SubCommandEnvelope(commandID: subCmdEnv.commandId, command: axSubCmd)