SwipeCommand.swiftβ’9.12 kB
import AppKit
import ArgumentParser
import AXorcist
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation
/// Performs swipe gestures using intelligent element finding and service-based architecture.
@available(macOS 14.0, *)
struct SwipeCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "swipe",
abstract: "Perform swipe gestures",
discussion: """
Performs a drag/swipe gesture between two points or elements.
Useful for drag-and-drop operations and gesture-based interactions.
EXAMPLES:
# Swipe between UI elements
peekaboo swipe --from B1 --to B5 --session-id 12345
# Swipe with coordinates
peekaboo swipe --from-coords 100,200 --to-coords 300,400
# Mixed mode: element to coordinates
peekaboo swipe --from T1 --to-coords 500,300 --duration 1000
# Slow swipe for precise gesture
peekaboo swipe --from-coords 50,50 --to-coords 400,400 --duration 2000
USAGE:
You can specify source and destination using either:
- Element IDs from a previous 'see' command
- Direct coordinates
- A mix of both
The swipe includes a configurable duration to control the
speed of the drag gesture.
""",
version: "2.0.0"
)
@Option(help: "Source element ID")
var from: String?
@Option(help: "Source coordinates (x,y)")
var fromCoords: String?
@Option(help: "Destination element ID")
var to: String?
@Option(help: "Destination coordinates (x,y)")
var toCoords: String?
@Option(help: "Session ID (uses latest if not specified)")
var session: String?
@Option(help: "Duration of the swipe in milliseconds")
var duration: Int = 500
@Option(help: "Number of intermediate points for smooth movement")
var steps: Int = 20
@Flag(help: "Use right mouse button for drag")
var rightButton = false
@Flag(help: "Output in JSON format")
var jsonOutput = false
mutating func run() async throws {
let startTime = Date()
Logger.shared.setJsonOutputMode(self.jsonOutput)
do {
// Validate inputs
guard self.from != nil || self.fromCoords != nil, self.to != nil || self.toCoords != nil else {
throw ArgumentParser.ValidationError(
"Must specify both source (--from or --from-coords) and destination (--to or --to-coords)"
)
}
// Note: Right-button swipe is not supported in the current implementation
if self.rightButton {
throw ArgumentParser
.ValidationError(
"Right-button swipe is not currently supported. " +
"Please use the standard swipe command for right-button gestures."
)
}
// Determine session ID - use provided or get most recent
let sessionId: String? = if let providedSession = session {
providedSession
} else {
await PeekabooServices.shared.sessions.getMostRecentSession()
}
// Get source and destination points
let sourcePoint = try await resolvePoint(
elementId: from,
coords: fromCoords,
sessionId: sessionId,
description: "from",
waitTimeout: 5.0
)
let destPoint = try await resolvePoint(
elementId: to,
coords: toCoords,
sessionId: sessionId,
description: "to",
waitTimeout: 5.0
)
// Perform swipe using UIAutomationService
try await PeekabooServices.shared.automation.swipe(
from: sourcePoint,
to: destPoint,
duration: self.duration,
steps: self.steps
)
// Calculate distance for output
let distance = sqrt(pow(destPoint.x - sourcePoint.x, 2) + pow(destPoint.y - sourcePoint.y, 2))
// Small delay to ensure swipe is processed
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
// Output results
if self.jsonOutput {
let output = SwipeResult(
success: true,
fromLocation: ["x": sourcePoint.x, "y": sourcePoint.y],
toLocation: ["x": destPoint.x, "y": destPoint.y],
distance: distance,
duration: self.duration,
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output)
} else {
print("β
Swipe completed")
print("π From: (\(Int(sourcePoint.x)), \(Int(sourcePoint.y)))")
print("π To: (\(Int(destPoint.x)), \(Int(destPoint.y)))")
print("π Distance: \(Int(distance)) pixels")
print("β±οΈ Duration: \(self.duration)ms")
print("β±οΈ Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s")
}
} catch {
self.handleError(error)
throw ExitCode.failure
}
}
private func resolvePoint(
elementId: String?,
coords: String?,
sessionId: String?,
description: String,
waitTimeout: TimeInterval
) async throws -> CGPoint {
if let coordString = coords {
// Parse coordinates
let parts = coordString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
guard parts.count == 2,
let x = Double(parts[0]),
let y = Double(parts[1])
else {
throw ArgumentParser.ValidationError("Invalid coordinates format: '\(coordString)'. Expected 'x,y'")
}
return CGPoint(x: x, y: y)
} else if let element = elementId, let activeSessionId = sessionId {
// Resolve from session using waitForElement
let target = ClickTarget.elementId(element)
let waitResult = try await PeekabooServices.shared.automation.waitForElement(
target: target,
timeout: waitTimeout,
sessionId: activeSessionId
)
if !waitResult.found {
throw PeekabooError.elementNotFound("Element with ID '\(element)' not found")
}
guard let foundElement = waitResult.element else {
throw PeekabooError.elementNotFound("Element '\(element)' found but has no bounds")
}
// Return center of element
return CGPoint(
x: foundElement.bounds.origin.x + foundElement.bounds.width / 2,
y: foundElement.bounds.origin.y + foundElement.bounds.height / 2
)
} else if elementId != nil {
throw ValidationError("Session ID required when using element IDs")
} else {
throw ValidationError("No \(description) point specified")
}
}
private func handleError(_ error: Error) {
if self.jsonOutput {
let errorCode: ErrorCode
let message: String
if let peekabooError = error as? PeekabooError {
switch peekabooError {
case .sessionNotFound:
errorCode = .SESSION_NOT_FOUND
message = "Session not found"
case .elementNotFound:
errorCode = .ELEMENT_NOT_FOUND
message = "Element not found"
case let .clickFailed(msg):
errorCode = .INTERACTION_FAILED
message = msg
case let .typeFailed(msg):
errorCode = .INTERACTION_FAILED
message = msg
default:
errorCode = .INTERNAL_SWIFT_ERROR
message = error.localizedDescription
}
} else if error is ArgumentParser.ValidationError {
errorCode = .INVALID_INPUT
message = error.localizedDescription
} else {
errorCode = .INTERNAL_SWIFT_ERROR
message = error.localizedDescription
}
outputError(message: message, code: errorCode)
} else {
var localStandardErrorStream = FileHandleTextOutputStream(FileHandle.standardError)
print("β Error: \(error.localizedDescription)", to: &localStandardErrorStream)
}
}
}
// MARK: - JSON Output Structure
struct SwipeResult: Codable {
let success: Bool
let fromLocation: [String: Double]
let toLocation: [String: Double]
let distance: Double
let duration: Int
let executionTime: TimeInterval
}