Skip to main content
Glama
ScrollCommand.swift7.91 kB
import Commander import CoreGraphics import Foundation import PeekabooCore import PeekabooFoundation /// Scrolls the mouse wheel in a specified direction. /// Supports scrolling on specific elements or at the current mouse position. @available(macOS 14.0, *) @MainActor struct ScrollCommand: ErrorHandlingCommand, OutputFormattable, RuntimeOptionsConfigurable { @Option(help: "Scroll direction: up, down, left, or right") var direction: String @Option(help: "Number of scroll ticks") var amount: Int = 3 @Option(help: "Element ID to scroll on (from 'see' command)") var on: String? @Option(help: "Session ID (uses latest if not specified)") var session: String? @Option(help: "Delay between scroll ticks in milliseconds") var delay: Int = 2 @Flag(help: "Use smooth scrolling with smaller increments") var smooth = false @Option(name: .long, help: "Target application to focus before scrolling") var app: String? @OptionGroup var focusOptions: FocusCommandOptions @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() private var resolvedRuntime: CommandRuntime { guard let runtime else { preconditionFailure("CommandRuntime must be configured before accessing runtime resources") } return runtime } private var services: any PeekabooServiceProviding { self.resolvedRuntime.services } private var logger: Logger { self.resolvedRuntime.logger } var outputLogger: Logger { self.logger } var jsonOutput: Bool { self.runtime?.configuration.jsonOutput ?? self.runtimeOptions.jsonOutput } @MainActor mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let startTime = Date() self.logger.setJsonOutputMode(self.jsonOutput) do { // Parse direction guard let scrollDirection = ScrollDirection(rawValue: direction.lowercased()) else { throw ValidationError("Invalid direction. Use: up, down, left, or right") } // Determine session ID if element target is specified let sessionId: String? = if self.on != nil { if let providedSession = session { providedSession } else { await self.services.sessions.getMostRecentSession() } } else { nil } // Ensure window is focused before scrolling try await ensureFocused( sessionId: sessionId, applicationName: self.app, options: self.focusOptions, services: self.services ) // Perform scroll using the service let scrollRequest = ScrollRequest( direction: scrollDirection, amount: self.amount, target: self.on, smooth: self.smooth, delay: self.delay, sessionId: sessionId ) try await AutomationServiceBridge.scroll( automation: self.services.automation, request: scrollRequest ) AutomationEventLogger.log( .scroll, "direction=\(self.direction) amount=\(self.amount) smooth=\(self.smooth) " + "target=\(self.on ?? "pointer") session=\(sessionId ?? "latest")" ) // Calculate total ticks for output let totalTicks = self.smooth ? self.amount * 3 : self.amount // Determine scroll location for output let scrollLocation: CGPoint = if let elementId = on { // Try to get element location from session if let activeSessionId = sessionId, let detectionResult = try? await self.services.sessions .getDetectionResult(sessionId: activeSessionId), let element = detectionResult.elements.findById(elementId) { CGPoint( x: element.bounds.midX, y: element.bounds.midY ) } else { // Fallback to zero if element not found (scroll still happened though) .zero } } else { // Get current mouse position CGEvent(source: nil)?.location ?? .zero } // Output results let outputPayload = ScrollResult( success: true, direction: direction, amount: amount, location: ["x": scrollLocation.x, "y": scrollLocation.y], totalTicks: totalTicks, executionTime: Date().timeIntervalSince(startTime) ) output(outputPayload) { print("✅ Scroll completed") print("🎯 Direction: \(self.direction)") print("📊 Amount: \(self.amount) ticks") if self.on != nil { print("📍 Location: (\(Int(scrollLocation.x)), \(Int(scrollLocation.y)))") } print("⏱️ Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s") } } catch { self.handleError(error) throw ExitCode.failure } } // Error handling is provided by ErrorHandlingCommand protocol } // MARK: - JSON Output Structure struct ScrollResult: Codable { let success: Bool let direction: String let amount: Int let location: [String: Double] let totalTicks: Int let executionTime: TimeInterval } // MARK: - Conformances @MainActor extension ScrollCommand: ParsableCommand { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "scroll", abstract: "Scroll the mouse wheel in any direction", discussion: """ The 'scroll' command simulates mouse wheel scrolling events. It can scroll up, down, left, or right by a specified amount. EXAMPLES: peekaboo scroll --direction down --amount 5 peekaboo scroll --direction up --amount 10 --on element_42 peekaboo scroll --direction right --amount 3 --smooth DIRECTION: up - Scroll content up (wheel down) down - Scroll content down (wheel up) left - Scroll content left right - Scroll content right AMOUNT: The number of scroll "lines" or "ticks" to perform. Each tick is equivalent to one notch on a physical mouse wheel. """, showHelpOnEmptyInvocation: true ) } } } extension ScrollCommand: AsyncRuntimeCommand {} @MainActor extension ScrollCommand: CommanderBindableCommand { mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { self.direction = try values.requireOption("direction", as: String.self) if let amount: Int = try values.decodeOption("amount", as: Int.self) { self.amount = amount } self.on = values.singleOption("on") self.session = values.singleOption("session") if let delay: Int = try values.decodeOption("delay", as: Int.self) { self.delay = delay } self.smooth = values.flag("smooth") self.app = values.singleOption("app") self.focusOptions = try values.makeFocusOptions() } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server