Skip to main content
Glama
PressCommand.swift8.11 kB
import Commander import Foundation import PeekabooCore import PeekabooFoundation /// Press individual keys or key sequences @available(macOS 14.0, *) @MainActor struct PressCommand: ErrorHandlingCommand, OutputFormattable, RuntimeOptionsConfigurable { @Argument(help: "Key(s) to press") var keys: [String] @Option(help: "Repeat count for all keys") var count: Int = 1 @Option(help: "Delay between key presses in milliseconds") var delay: Int = 100 @Option(help: "Hold duration for each key in milliseconds") var hold: Int = 50 @Option(help: "Session ID (uses latest if not specified)") var session: String? @OptionGroup var focusOptions: FocusCommandOptions @RuntimeStorage private var runtime: CommandRuntime? var runtimeOptions = CommandRuntimeOptions() private var resolvedRuntime: CommandRuntime { if let runtime { return runtime } // Parsing-only code paths in tests may access runtime-dependent helpers; default lazily. return CommandRuntime.makeDefault(options: self.runtimeOptions) } private var configuration: CommandRuntime.Configuration { if let runtime { return runtime.configuration } // Unit tests may parse without a runtime; fall back to parsed runtime options. return self.runtimeOptions.makeConfiguration() } private var services: any PeekabooServiceProviding { self.resolvedRuntime.services } private var logger: Logger { self.resolvedRuntime.logger } var outputLogger: Logger { self.logger } var jsonOutput: Bool { self.configuration.jsonOutput } @MainActor mutating func run(using runtime: CommandRuntime) async throws { self.runtime = runtime let startTime = Date() self.logger.setJsonOutputMode(self.jsonOutput) do { try self.validate() // Get session if available let sessionId: String? = if let providedSession = session { providedSession } else { await self.services.sessions.getMostRecentSession() } // Ensure window is focused before pressing keys if let sessionId { try await ensureFocused( sessionId: sessionId, options: self.focusOptions, services: self.services ) } // Build actions - repeat each key sequence 'count' times var actions: [TypeAction] = [] for _ in 0..<self.count { for (index, key) in self.keys.indexed() { if let specialKey = SpecialKey(rawValue: key.lowercased()) { actions.append(.key(specialKey)) } // Add delay between keys (but not after the last key of the last repetition) let isLastKey = index == self.keys.count - 1 let isLastRepetition = self.count == 1 if !isLastKey || !isLastRepetition { // We'll handle the delay in the service } } } // Execute key presses let typeRequest = TypeActionsRequest( actions: actions, cadence: .fixed(milliseconds: self.delay), sessionId: sessionId ) let result = try await AutomationServiceBridge.typeActions( automation: self.services.automation, request: typeRequest ) // Output results let pressResult = PressResult( success: true, keys: keys, totalPresses: result.keyPresses, count: self.count, executionTime: Date().timeIntervalSince(startTime) ) output(pressResult) { print("✅ Key press completed") print("🔑 Keys: \(self.keys.joined(separator: " → "))") if self.count > 1 { print("🔢 Repeated: \(self.count) times") } print("📊 Total presses: \(result.keyPresses)") print("⏱️ Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s") } } catch { self.handleError(error) throw ExitCode.failure } } // Error handling is provided by ErrorHandlingCommand protocol mutating func validate() throws { for key in self.keys { guard SpecialKey(rawValue: key.lowercased()) != nil else { throw ValidationError("Unknown key: '\(key)'. Run 'peekaboo press --help' for available keys.") } } } } // MARK: - JSON Output Structure struct PressResult: Codable { let success: Bool let keys: [String] let totalPresses: Int let count: Int let executionTime: TimeInterval } // MARK: - Conformances @MainActor extension PressCommand: ParsableCommand { nonisolated(unsafe) static var commandDescription: CommandDescription { MainActorCommandDescription.describe { CommandDescription( commandName: "press", abstract: "Press individual keys or key sequences", discussion: """ The 'press' command sends individual key presses or sequences. It's designed for special keys and navigation, not for typing text. EXAMPLES: peekaboo press return # Press Enter/Return peekaboo press tab --count 3 # Press Tab 3 times peekaboo press escape # Press Escape peekaboo press delete # Press Backspace/Delete peekaboo press forward_delete # Press Forward Delete (fn+delete) peekaboo press up down left right # Arrow key sequence peekaboo press f1 # Press F1 function key peekaboo press space # Press spacebar peekaboo press enter # Numeric keypad Enter AVAILABLE KEYS: Navigation: up, down, left, right, home, end, pageup, pagedown Editing: delete (backspace), forward_delete, clear Control: return, enter, tab, escape, space Function: f1-f12 Special: caps_lock, help KEY SEQUENCES: Multiple keys can be pressed in sequence with optional delay: peekaboo press tab tab return # Tab twice then Enter peekaboo press down down return # Navigate down and select TIMING: Use --delay to control timing between key presses (default: 100ms) Use --hold to control how long each key is held (default: 50ms) """, showHelpOnEmptyInvocation: true ) } } } extension PressCommand: AsyncRuntimeCommand {} @MainActor extension PressCommand: CommanderBindableCommand { mutating func applyCommanderValues(_ values: CommanderBindableValues) throws { guard !values.positional.isEmpty else { throw CommanderBindingError.missingArgument(label: "keys") } self.keys = values.positional if let count: Int = try values.decodeOption("count", as: Int.self) { self.count = count } if let delay: Int = try values.decodeOption("delay", as: Int.self) { self.delay = delay } if let hold: Int = try values.decodeOption("hold", as: Int.self) { self.hold = hold } self.session = values.singleOption("session") 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