swiftformat
This commit is contained in:
parent
3bdb9f7b53
commit
4826d2bfa3
@ -117,27 +117,27 @@ func exampleObserverCenter() {
|
||||
@MainActor
|
||||
func exampleModernActions() {
|
||||
guard let app = frontmostApplicationElement() else { return }
|
||||
|
||||
|
||||
do {
|
||||
// Old way (still supported):
|
||||
// try app.performAction("AXPress")
|
||||
// try app.performAction(AXActionNames.kAXRaiseAction)
|
||||
|
||||
|
||||
// New way with cleaner enum syntax:
|
||||
try app.performAction(.press)
|
||||
print("✅ Pressed element successfully")
|
||||
|
||||
|
||||
try app.performAction(.raise)
|
||||
print("✅ Raised element successfully")
|
||||
|
||||
|
||||
// All available actions:
|
||||
let availableActions: [AXAction] = [
|
||||
.press, .increment, .decrement, .confirm, .cancel,
|
||||
.showMenu, .pick, .raise, .setValue
|
||||
]
|
||||
|
||||
|
||||
print("Available actions: \(availableActions.map(\.rawValue).joined(separator: ", "))")
|
||||
|
||||
|
||||
} catch {
|
||||
print("❌ Action failed: \(error)")
|
||||
}
|
||||
|
||||
@ -49,34 +49,34 @@ import Foundation
|
||||
public enum AXCommand: Sendable {
|
||||
/// Searches for UI elements matching specified criteria.
|
||||
case query(QueryCommand)
|
||||
|
||||
|
||||
/// Performs an accessibility action on a target element.
|
||||
case performAction(PerformActionCommand)
|
||||
|
||||
|
||||
/// Retrieves specific accessibility attributes from an element.
|
||||
case getAttributes(GetAttributesCommand)
|
||||
|
||||
|
||||
/// Provides detailed information about an element's structure and properties.
|
||||
case describeElement(DescribeElementCommand)
|
||||
|
||||
|
||||
/// Extracts text content from an element and its descendants.
|
||||
case extractText(ExtractTextCommand)
|
||||
|
||||
|
||||
/// Executes multiple commands in a single batch operation.
|
||||
case batch(AXBatchCommand)
|
||||
|
||||
|
||||
/// Sets the value of the currently focused element.
|
||||
case setFocusedValue(SetFocusedValueCommand)
|
||||
|
||||
|
||||
/// Finds the UI element at specific screen coordinates.
|
||||
case getElementAtPoint(GetElementAtPointCommand)
|
||||
|
||||
|
||||
/// Retrieves the currently focused accessibility element.
|
||||
case getFocusedElement(GetFocusedElementCommand)
|
||||
|
||||
|
||||
/// Observes accessibility notifications from specified elements.
|
||||
case observe(ObserveCommand)
|
||||
|
||||
|
||||
/// Collects all elements from an application with optional filtering.
|
||||
case collectAll(CollectAllCommand)
|
||||
|
||||
@ -132,7 +132,7 @@ public struct AXCommandEnvelope: Sendable {
|
||||
/// Used for tracking, logging, and correlating commands with their responses.
|
||||
/// Should be unique across command executions for proper traceability.
|
||||
public let commandID: String
|
||||
|
||||
|
||||
/// The accessibility command to execute.
|
||||
///
|
||||
/// Contains the specific operation and its parameters that will be
|
||||
@ -386,7 +386,7 @@ public struct AXBatchCommand: Sendable {
|
||||
public struct SubCommandEnvelope: Sendable {
|
||||
/// Unique identifier for this sub-command within the batch.
|
||||
public let commandID: String
|
||||
|
||||
|
||||
/// The accessibility command to execute.
|
||||
public let command: AXCommand
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ public enum AXAction {
|
||||
case pick
|
||||
case raise
|
||||
case setValue
|
||||
|
||||
|
||||
/// The raw string value for the action
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
|
||||
@ -53,71 +53,71 @@ public struct CommandEnvelope: Codable {
|
||||
/// This ID is used for tracking and correlating commands with their responses,
|
||||
/// especially useful in batch operations or asynchronous processing.
|
||||
public let commandId: String
|
||||
|
||||
|
||||
/// The type of command to execute.
|
||||
///
|
||||
/// Determines what operation will be performed (query, action, observation, etc.).
|
||||
public let command: CommandType
|
||||
|
||||
|
||||
/// Target application name or bundle identifier.
|
||||
///
|
||||
/// Specifies which application the command should operate on. Can be an
|
||||
/// application name like "Safari" or a bundle identifier like "com.apple.Safari".
|
||||
public let application: String?
|
||||
|
||||
|
||||
/// Specific attributes to retrieve or filter by.
|
||||
///
|
||||
/// When provided, limits the operation to only the specified accessibility attributes.
|
||||
public let attributes: [String]?
|
||||
|
||||
|
||||
/// Key-value payload for compatibility with ping operations.
|
||||
public let payload: [String: String]?
|
||||
|
||||
|
||||
/// Whether to enable debug logging for this command.
|
||||
public let debugLogging: Bool
|
||||
|
||||
|
||||
/// Element locator specifying how to find the target element.
|
||||
///
|
||||
/// Provides search criteria for identifying specific UI elements within the application.
|
||||
public let locator: Locator?
|
||||
|
||||
|
||||
/// Legacy path hint for element location (deprecated).
|
||||
///
|
||||
/// > Important: Use ``locator`` instead of this property for new code.
|
||||
public let pathHint: [String]?
|
||||
|
||||
|
||||
/// Maximum number of elements to return in search results.
|
||||
public let maxElements: Int?
|
||||
|
||||
|
||||
/// Maximum depth for hierarchical searches.
|
||||
///
|
||||
/// Controls how deep into the UI element tree the search should traverse.
|
||||
public let maxDepth: Int?
|
||||
|
||||
|
||||
/// Output format for the command response.
|
||||
public let outputFormat: OutputFormat?
|
||||
|
||||
|
||||
/// Name of the action to perform for action commands.
|
||||
///
|
||||
/// Examples: "AXPress", "AXShowMenu", "AXScrollToVisible"
|
||||
public let actionName: String?
|
||||
|
||||
|
||||
/// Value parameter for action commands.
|
||||
///
|
||||
/// Some actions require additional parameters (e.g., scroll amount, text to enter).
|
||||
public let actionValue: AnyCodable?
|
||||
|
||||
|
||||
/// Sub-commands for batch operations.
|
||||
///
|
||||
/// When present, this command represents a batch operation containing
|
||||
/// multiple individual commands to execute in sequence.
|
||||
public let subCommands: [CommandEnvelope]?
|
||||
|
||||
|
||||
/// Screen coordinates for point-based operations.
|
||||
///
|
||||
/// Used with commands that need to interact with elements at specific screen locations.
|
||||
public let point: CGPoint?
|
||||
|
||||
|
||||
/// Process ID for targeting specific application instances.
|
||||
///
|
||||
/// When multiple instances of an application are running, this specifies
|
||||
@ -125,17 +125,17 @@ public struct CommandEnvelope: Codable {
|
||||
public let pid: Int?
|
||||
|
||||
// MARK: - Observation Parameters
|
||||
|
||||
|
||||
/// Notification types to observe for observation commands.
|
||||
///
|
||||
/// List of accessibility notification names to monitor (e.g., "AXValueChanged").
|
||||
public let notifications: [String]?
|
||||
|
||||
|
||||
/// Element details to include in observation notifications.
|
||||
///
|
||||
/// Specifies which element attributes should be included when notifications are triggered.
|
||||
public let includeElementDetails: [String]?
|
||||
|
||||
|
||||
/// Whether to monitor child elements for notifications.
|
||||
///
|
||||
/// When true, notifications from child elements will also be captured.
|
||||
@ -230,21 +230,21 @@ public struct CommandEnvelope: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
commandId = try container.decode(String.self, forKey: .commandId)
|
||||
command = try container.decode(CommandType.self, forKey: .command)
|
||||
command = try container.decode(CommandType.self, forKey: .command)
|
||||
application = try container.decodeIfPresent(String.self, forKey: .application)
|
||||
attributes = try container.decodeIfPresent([String].self, forKey: .attributes)
|
||||
payload = try container.decodeIfPresent([String:String].self, forKey: .payload)
|
||||
attributes = try container.decodeIfPresent([String].self, forKey: .attributes)
|
||||
payload = try container.decodeIfPresent([String:String].self, forKey: .payload)
|
||||
debugLogging = try container.decodeIfPresent(Bool.self, forKey: .debugLogging) ?? false
|
||||
locator = try container.decodeIfPresent(Locator.self, forKey: .locator)
|
||||
pathHint = try container.decodeIfPresent([String].self, forKey: .pathHint)
|
||||
locator = try container.decodeIfPresent(Locator.self, forKey: .locator)
|
||||
pathHint = try container.decodeIfPresent([String].self, forKey: .pathHint)
|
||||
maxElements = try container.decodeIfPresent(Int.self, forKey: .maxElements)
|
||||
maxDepth = try container.decodeIfPresent(Int.self, forKey: .maxDepth)
|
||||
maxDepth = try container.decodeIfPresent(Int.self, forKey: .maxDepth)
|
||||
outputFormat = try container.decodeIfPresent(OutputFormat.self, forKey: .outputFormat)
|
||||
actionName = try container.decodeIfPresent(String.self, forKey: .actionName)
|
||||
actionName = try container.decodeIfPresent(String.self, forKey: .actionName)
|
||||
actionValue = try container.decodeIfPresent(AnyCodable.self, forKey: .actionValue)
|
||||
subCommands = try container.decodeIfPresent([CommandEnvelope].self, forKey: .subCommands)
|
||||
point = try container.decodeIfPresent(CGPoint.self, forKey: .point)
|
||||
pid = try container.decodeIfPresent(Int.self, forKey: .pid)
|
||||
point = try container.decodeIfPresent(CGPoint.self, forKey: .point)
|
||||
pid = try container.decodeIfPresent(Int.self, forKey: .pid)
|
||||
notifications = try container.decodeIfPresent([String].self, forKey: .notifications)
|
||||
includeElementDetails = try container.decodeIfPresent([String].self, forKey: .includeElementDetails)
|
||||
watchChildren = try container.decodeIfPresent(Bool.self, forKey: .watchChildren)
|
||||
|
||||
@ -10,7 +10,7 @@ public extension Element {
|
||||
func isWindowMinimized() -> Bool? {
|
||||
return isMinimized()
|
||||
}
|
||||
|
||||
|
||||
/// Checks if the window is hidden (different from minimized - this is when the app is hidden with Cmd+H)
|
||||
/// - Returns: true if hidden, false if not, nil if cannot be determined
|
||||
func isWindowHidden() -> Bool? {
|
||||
@ -22,17 +22,17 @@ public extension Element {
|
||||
}
|
||||
current = element.parent()
|
||||
}
|
||||
|
||||
|
||||
// Alternative: If we have the PID, we can create the application element directly
|
||||
if let windowPid = self.pid() {
|
||||
if let app = Element.application(for: windowPid) {
|
||||
return app.attribute(Attribute<Bool>("AXHidden"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Minimizes the window
|
||||
/// - Returns: AXError indicating success or failure
|
||||
func minimizeWindow() -> AXError {
|
||||
@ -41,7 +41,7 @@ public extension Element {
|
||||
if error == .success {
|
||||
return .success
|
||||
}
|
||||
|
||||
|
||||
// Fallback: Try to press the minimize button
|
||||
if let minimizeBtn = minimizeButton() {
|
||||
do {
|
||||
@ -51,18 +51,18 @@ public extension Element {
|
||||
return .actionUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
|
||||
/// Unminimizes the window
|
||||
/// - Returns: AXError indicating success or failure
|
||||
func unminimizeWindow() -> AXError {
|
||||
return setMinimized(false)
|
||||
}
|
||||
|
||||
|
||||
/// Closes the window
|
||||
/// - Returns: AXError indicating success or failure
|
||||
/// - Returns: AXError indicating success or failure
|
||||
func closeWindow() -> AXError {
|
||||
// Try to press the close button
|
||||
if let closeBtn = closeButton() {
|
||||
@ -73,7 +73,7 @@ public extension Element {
|
||||
return .actionUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback: Try the close action if available
|
||||
if let supportedActions = supportedActions(), supportedActions.contains("AXClose") {
|
||||
do {
|
||||
@ -83,10 +83,10 @@ public extension Element {
|
||||
return .actionUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return .actionUnsupported
|
||||
}
|
||||
|
||||
|
||||
/// Brings the window to front
|
||||
/// - Returns: AXError indicating success or failure
|
||||
func raiseWindow() -> AXError {
|
||||
@ -110,49 +110,49 @@ public extension Element {
|
||||
if isMinimized() == true {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Get window frame
|
||||
guard let windowFrame = frame() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Handle case where window might be hidden
|
||||
if isWindowHidden() == true {
|
||||
// Hidden windows maintain their position, so we can still determine the screen
|
||||
return screenContainingRect(windowFrame)
|
||||
}
|
||||
|
||||
|
||||
return screenContainingRect(windowFrame)
|
||||
}
|
||||
|
||||
|
||||
/// Gets the screen number (1-based) that contains this window
|
||||
/// - Returns: The screen number, or nil if the window is minimized or cannot be determined
|
||||
func windowScreenNumber() -> Int? {
|
||||
guard let screen = windowScreen() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let screens = NSScreen.screens
|
||||
for (index, s) in screens.enumerated() {
|
||||
if s == screen {
|
||||
return index + 1 // 1-based numbering
|
||||
return index + 1 // 1-based numbering
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Determines which screen contains the largest portion of the given rect
|
||||
/// - Parameter rect: The rect to check
|
||||
/// - Returns: The screen containing the largest portion of the rect, or nil if no intersection
|
||||
private func screenContainingRect(_ rect: CGRect) -> NSScreen? {
|
||||
var bestScreen: NSScreen?
|
||||
var bestArea: CGFloat = 0
|
||||
|
||||
|
||||
for screen in NSScreen.screens {
|
||||
let screenFrame = screen.frame
|
||||
let intersection = screenFrame.intersection(rect)
|
||||
|
||||
|
||||
if !intersection.isNull {
|
||||
let area = intersection.width * intersection.height
|
||||
if area > bestArea {
|
||||
@ -161,7 +161,7 @@ public extension Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no intersection found, check by window center point
|
||||
if bestScreen == nil {
|
||||
let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
|
||||
@ -171,10 +171,10 @@ public extension Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return bestScreen
|
||||
}
|
||||
|
||||
|
||||
/// Gets detailed screen information for the window
|
||||
/// - Returns: A dictionary containing screen information, or nil if cannot be determined
|
||||
func windowScreenInfo() -> [String: Any]? {
|
||||
@ -182,7 +182,7 @@ public extension Element {
|
||||
// Check if minimized or hidden
|
||||
let isMin = isMinimized() ?? false
|
||||
let isHid = isWindowHidden() ?? false
|
||||
|
||||
|
||||
return [
|
||||
"screenNumber": NSNull(),
|
||||
"isMinimized": isMin,
|
||||
@ -190,11 +190,11 @@ public extension Element {
|
||||
"hasScreen": false
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
let screenNumber = windowScreenNumber() ?? 0
|
||||
let screenFrame = screen.frame
|
||||
let visibleFrame = screen.visibleFrame
|
||||
|
||||
|
||||
return [
|
||||
"screenNumber": screenNumber,
|
||||
"screenFrame": NSStringFromRect(screenFrame),
|
||||
@ -219,20 +219,20 @@ public extension Element {
|
||||
if let minimized = isMinimized(), minimized {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Check if hidden
|
||||
if let hidden = isWindowHidden(), hidden {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Check if it has a valid frame on a screen
|
||||
if let _ = windowScreen() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Shows a hidden window (unhides the application if needed)
|
||||
/// - Returns: AXError indicating success or failure
|
||||
func showWindow() -> AXError {
|
||||
@ -243,11 +243,11 @@ public extension Element {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Then unhide the app if needed
|
||||
// Get the application element by walking up the parent hierarchy or using PID
|
||||
var appElement: Element? = nil
|
||||
|
||||
|
||||
// Try parent hierarchy first
|
||||
var current: Element? = self
|
||||
while let element = current {
|
||||
@ -257,20 +257,20 @@ public extension Element {
|
||||
}
|
||||
current = element.parent()
|
||||
}
|
||||
|
||||
|
||||
// If not found, try using PID
|
||||
if appElement == nil, let windowPid = self.pid() {
|
||||
appElement = Element.application(for: windowPid)
|
||||
}
|
||||
|
||||
|
||||
if let app = appElement, app.attribute(Attribute<Bool>("AXHidden")) == true {
|
||||
let error = AXUIElementSetAttributeValue(app.underlyingElement, "AXHidden" as CFString, false as CFBoolean)
|
||||
if error != AXError.success {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finally raise the window
|
||||
return raiseWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,13 +52,13 @@ public struct Element: Equatable, Hashable {
|
||||
/// When populated (typically by deep queries), this contains all the
|
||||
/// accessibility attributes for the element, avoiding repeated API calls.
|
||||
public var attributes: [String: AnyCodable]?
|
||||
|
||||
|
||||
/// Pre-fetched child elements.
|
||||
///
|
||||
/// When populated by deep queries, this contains all direct child elements,
|
||||
/// allowing for efficient tree traversal without additional API calls.
|
||||
public var prefetchedChildren: [Element]?
|
||||
|
||||
|
||||
/// Pre-fetched available actions for this element.
|
||||
///
|
||||
/// When populated, this contains all actions that can be performed on
|
||||
|
||||
@ -14,7 +14,7 @@ public typealias AXNotificationSubscriptionHandler = @MainActor (/*element: Elem
|
||||
public struct AXNotificationSubscriptionKey: Hashable {
|
||||
/// Process ID to monitor, or nil for global monitoring.
|
||||
let pid: pid_t?
|
||||
|
||||
|
||||
/// The accessibility notification type to observe.
|
||||
let notification: AXNotification
|
||||
}
|
||||
@ -26,7 +26,7 @@ public struct AXNotificationSubscriptionKey: Hashable {
|
||||
public struct AXObserverKeyAndPID: Hashable {
|
||||
/// Process ID being observed.
|
||||
let pid: pid_t
|
||||
|
||||
|
||||
/// Notification type being monitored.
|
||||
let key: AXNotification
|
||||
}
|
||||
@ -38,7 +38,7 @@ public struct AXObserverKeyAndPID: Hashable {
|
||||
public struct AXObserverObjAndPID {
|
||||
/// The active accessibility observer.
|
||||
var observer: AXObserver
|
||||
|
||||
|
||||
/// Process ID that this observer is monitoring.
|
||||
var pid: pid_t
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ extension HandlerResponse {
|
||||
/// message: "Element not found",
|
||||
/// underlyingError: "AXUIElementCopyAttributeValue failed"
|
||||
/// )
|
||||
///
|
||||
///
|
||||
/// let response = HandlerResponse(
|
||||
/// data: AnyCodable(detailedError),
|
||||
/// error: "Element lookup failed"
|
||||
@ -101,12 +101,12 @@ public struct DetailedError: Codable, Sendable {
|
||||
/// Use consistent error codes across your application to enable
|
||||
/// programmatic error handling.
|
||||
public let code: Int
|
||||
|
||||
|
||||
/// Human-readable error message describing what went wrong.
|
||||
///
|
||||
/// This should be clear and actionable for developers debugging issues.
|
||||
public let message: String
|
||||
|
||||
|
||||
/// Additional details about underlying system errors.
|
||||
///
|
||||
/// When the error is caused by a lower-level system call or API,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user