Skip to main content
Glama

Peekaboo MCP

by steipete
WindowCommand.swiftโ€ข23.9 kB
import AppKit import ArgumentParser import CoreGraphics import Foundation import PeekabooCore import PeekabooFoundation // Logger for window command debugging private let logger = Logger.shared /// Manipulate application windows with various actions struct WindowCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "window", abstract: "Manipulate application windows", discussion: """ SYNOPSIS: peekaboo window SUBCOMMAND [OPTIONS] DESCRIPTION: Provides window manipulation capabilities including closing, minimizing, maximizing, moving, resizing, and focusing windows. EXAMPLES: # Close a window peekaboo window close --app Safari peekaboo window close --app Safari --window-title "GitHub" # Minimize/maximize windows peekaboo window minimize --app Finder peekaboo window maximize --app Terminal # Move and resize windows peekaboo window move --app TextEdit --x 100 --y 100 peekaboo window resize --app Safari --width 1200 --height 800 peekaboo window set-bounds --app Chrome --x 50 --y 50 --width 1024 --height 768 # Focus a window peekaboo window focus --app "Visual Studio Code" peekaboo window focus --app Safari --window-title "Apple" # List windows (convenience shortcut) peekaboo window list --app Safari SUBCOMMANDS: close Close a window minimize Minimize a window to the Dock maximize Maximize a window (full screen) move Move a window to a new position resize Resize a window set-bounds Set window position and size in one operation focus Bring a window to the foreground list List windows for an application OUTPUT FORMAT: Default output is human-readable text. Use --json-output for machine-readable JSON format. """, subcommands: [ CloseSubcommand.self, MinimizeSubcommand.self, MaximizeSubcommand.self, MoveSubcommand.self, ResizeSubcommand.self, SetBoundsSubcommand.self, FocusSubcommand.self, WindowListSubcommand.self, ] ) } // MARK: - Common Options struct WindowIdentificationOptions: ParsableArguments, ApplicationResolvable { @Option(name: .long, help: "Target application name, bundle ID, or 'PID:12345'") var app: String? @Option(name: .long, help: "Target application by process ID") var pid: Int32? @Option(name: .long, help: "Target window by title (partial match supported)") var windowTitle: String? @Option(name: .long, help: "Target window by index (0-based, frontmost is 0)") var windowIndex: Int? func validate() throws { // Ensure we have some way to identify the window if self.app == nil && self.pid == nil { throw ValidationError("Either --app or --pid must be specified") } } /// Convert to WindowTarget for service layer func toWindowTarget() throws -> WindowTarget { let appIdentifier = try self.resolveApplicationIdentifier() if let index = windowIndex { return .index(app: appIdentifier, index: index) } else if self.windowTitle != nil { // For title matching, we still need the app context // The service will handle finding the right window return .application(appIdentifier) } else { // Default to app's frontmost window return .application(appIdentifier) } } } // MARK: - Helper Functions private func createWindowActionResult( action: String, success: Bool, windowInfo: ServiceWindowInfo?, appName: String? = nil ) -> WindowActionResult { let bounds: WindowBounds? = if let windowInfo { WindowBounds( x: Int(windowInfo.bounds.origin.x), y: Int(windowInfo.bounds.origin.y), width: Int(windowInfo.bounds.size.width), height: Int(windowInfo.bounds.size.height) ) } else { nil } return WindowActionResult( action: action, success: success, app_name: appName ?? "Unknown", window_title: windowInfo?.title, new_bounds: bounds ) } // MARK: - Subcommands struct CloseSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "close", abstract: "Close a window" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info before action let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Perform the action try await PeekabooServices.shared.windows.closeWindow(target: target) let data = createWindowActionResult( action: "close", success: true, windowInfo: windowInfo, appName: appName ) output(data) { print("Successfully closed window '\(windowInfo?.title ?? "Untitled")' of \(appName)") } } catch { handleError(error) throw ExitCode(1) } } } struct MinimizeSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "minimize", abstract: "Minimize a window to the Dock" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info before action let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Perform the action try await PeekabooServices.shared.windows.minimizeWindow(target: target) let data = createWindowActionResult( action: "minimize", success: true, windowInfo: windowInfo, appName: appName ) output(data) { print("Successfully minimized window '\(windowInfo?.title ?? "Untitled")' of \(appName)") } } catch { handleError(error) throw ExitCode(1) } } } struct MaximizeSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "maximize", abstract: "Maximize a window (full screen)" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info before action let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Perform the action try await PeekabooServices.shared.windows.maximizeWindow(target: target) let data = createWindowActionResult( action: "maximize", success: true, windowInfo: windowInfo, appName: appName ) output(data) { print("Successfully maximized window '\(windowInfo?.title ?? "Untitled")' of \(appName)") } } catch { handleError(error) throw ExitCode(1) } } } struct FocusSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "focus", abstract: "Bring a window to the foreground", discussion: """ Focus brings a window to the foreground and activates its application. Space Support: By default, if the window is on a different Space (virtual desktop), the focus command will switch to that Space. You can control this behavior with the --space-switch and --move-here options. Examples: peekaboo window focus --app Safari peekaboo window focus --app "Visual Studio Code" --window-title "main.swift" peekaboo window focus --app Terminal --no-space-switch peekaboo window focus --app Finder --move-here """ ) @OptionGroup var windowOptions: WindowIdentificationOptions @OptionGroup var focusOptions: FocusOptions @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { logger.debug("FocusSubcommand.run() called") Logger.shared.setJsonOutputMode(self.jsonOutput) do { logger.debug("About to validate window options") try self.windowOptions.validate() logger.debug("Window options validated") let target = self.windowOptions.createTarget() logger.debug("Target created: \(target)") // Get window info before action let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) logger.debug("Found \(windows.count) windows") let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Check if we found any windows guard !windows.isEmpty else { throw PeekabooError.windowNotFound(criteria: "No windows found for \(appName)") } // Use enhanced focus with space support if let windowID = windowInfo?.windowID { try await ensureFocused( windowID: CGWindowID(windowID), applicationName: self.windowOptions.app, windowTitle: self.windowOptions.windowTitle, options: self.focusOptions ) } else { // Fallback to regular focus if no window ID try await PeekabooServices.shared.windows.focusWindow(target: target) } let data = createWindowActionResult( action: "focus", success: true, windowInfo: windowInfo, appName: appName ) output(data) { var message = "Successfully focused window '\(windowInfo?.title ?? "Untitled")' of \(appName)" if self.focusOptions.bringToCurrentSpace { message += " (moved to current Space)" } print(message) } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Move Command struct MoveSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "move", abstract: "Move a window to a new position" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Option(name: .short, help: "New X coordinate") var x: Int @Option(name: .short, help: "New Y coordinate") var y: Int @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Move the window let newOrigin = CGPoint(x: x, y: y) try await PeekabooServices.shared.windows.moveWindow(target: target, to: newOrigin) // Create result with new bounds let updatedInfo = windowInfo.map { info in ServiceWindowInfo( windowID: info.windowID, title: info.title, bounds: CGRect(origin: newOrigin, size: info.bounds.size), isMinimized: info.isMinimized, isMainWindow: info.isMainWindow, windowLevel: info.windowLevel, alpha: info.alpha, index: info.index ) } let data = createWindowActionResult( action: "move", success: true, windowInfo: updatedInfo, appName: appName ) output(data) { print("Successfully moved window '\(windowInfo?.title ?? "Untitled")' to (\(self.x), \(self.y))") } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Resize Command struct ResizeSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "resize", abstract: "Resize a window" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Option(name: .short, help: "New width") var width: Int @Option(name: .long, help: "New height") var height: Int @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Resize the window let newSize = CGSize(width: width, height: height) try await PeekabooServices.shared.windows.resizeWindow(target: target, to: newSize) // We'll pass the original windowInfo as-is since window service would have updated it // (In a real implementation, we'd refetch window info after resize) let data = createWindowActionResult( action: "resize", success: true, windowInfo: windowInfo, appName: appName ) output(data) { print("Successfully resized window '\(windowInfo?.title ?? "Untitled")' to \(self.width)x\(self.height)" ) } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Set Bounds Command struct SetBoundsSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "set-bounds", abstract: "Set window position and size in one operation" ) @OptionGroup var windowOptions: WindowIdentificationOptions @Option(name: .short, help: "New X coordinate") var x: Int @Option(name: .short, help: "New Y coordinate") var y: Int @Option(name: .short, help: "New width") var width: Int @Option(name: .long, help: "New height") var height: Int @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { try self.windowOptions.validate() let target = self.windowOptions.createTarget() // Get window info let windows = try await PeekabooServices.shared.windows .listWindows(target: self.windowOptions.toWindowTarget()) let windowInfo = self.windowOptions.selectWindow(from: windows) let appName = self.windowOptions.app ?? "Unknown" // Set bounds let newBounds = CGRect(x: x, y: y, width: width, height: height) try await PeekabooServices.shared.windows.setWindowBounds(target: target, bounds: newBounds) // We'll pass the original windowInfo as-is since window service would have updated it // (In a real implementation, we'd refetch window info after set-bounds) let data = createWindowActionResult( action: "set-bounds", success: true, windowInfo: windowInfo, appName: appName ) output(data) { print( "Successfully set window '\(windowInfo?.title ?? "Untitled")' bounds to (\(self.x), \(self.y)) \(self.width)x\(self.height)" ) } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - List Command struct WindowListSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable, ApplicationResolvable { static let configuration = CommandConfiguration( commandName: "list", abstract: "List windows for an application" ) @Option(name: .long, help: "Target application name, bundle ID, or 'PID:12345'") var app: String? @Option(name: .long, help: "Target application by process ID") var pid: Int32? @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false @Flag(name: .long, help: "Group windows by Space (virtual desktop)") var groupBySpace = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { let appIdentifier = try self.resolveApplicationIdentifier() // First find the application to get its info let appInfo = try await PeekabooServices.shared.applications.findApplication(identifier: appIdentifier) let target = WindowTarget.application(appIdentifier) let windows = try await PeekabooServices.shared.windows.listWindows(target: target) // Convert ServiceWindowInfo to WindowInfo for consistency let windowInfos = windows.enumerated().map { index, window in WindowInfo( window_title: window.title, window_id: UInt32(window.windowID), window_index: index, bounds: WindowBounds( x: Int(window.bounds.origin.x), y: Int(window.bounds.origin.y), width: Int(window.bounds.size.width), height: Int(window.bounds.size.height) ), is_on_screen: !window.isMinimized ) } // Use PeekabooCore's WindowListData let data = WindowListData( windows: windowInfos, target_application_info: TargetApplicationInfo( app_name: appInfo.name, bundle_id: appInfo.bundleIdentifier, pid: appInfo.processIdentifier ) ) output(data) { print("\(data.target_application_info.app_name) has \(data.windows.count) window(s):") if self.groupBySpace { // Group windows by space var windowsBySpace: [UInt64?: [(window: ServiceWindowInfo, index: Int)]] = [:] for (index, window) in windows.enumerated() { let spaceID = window.spaceID if windowsBySpace[spaceID] == nil { windowsBySpace[spaceID] = [] } windowsBySpace[spaceID]?.append((window, index)) } // Sort spaces by ID (nil first for windows not on any space) let sortedSpaces = windowsBySpace.keys.sorted { a, b in switch (a, b) { case (nil, nil): false case (nil, _): true case (_, nil): false case let (a?, b?): a < b } } // Print grouped windows for spaceID in sortedSpaces { if let spaceID { let spaceName = windowsBySpace[spaceID]?.first?.window.spaceName ?? "Space \(spaceID)" print("\n Space: \(spaceName) [ID: \(spaceID)]") } else { print("\n No Space:") } for (window, index) in windowsBySpace[spaceID] ?? [] { let status = window.isMinimized ? " [minimized]" : "" print(" [\(index)] \"\(window.title)\"\(status)") print(" Position: (\(Int(window.bounds.origin.x)), \(Int(window.bounds.origin.y)))") print(" Size: \(Int(window.bounds.size.width))x\(Int(window.bounds.size.height))") } } } else { // Original flat list for window in data.windows { let index = window.window_index ?? 0 let status = (window.is_on_screen == false) ? " [minimized]" : "" print(" [\(index)] \"\(window.window_title)\"\(status)") if let bounds = window.bounds { print(" Position: (\(bounds.x), \(bounds.y))") print(" Size: \(bounds.width)x\(bounds.height)") } } } } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Response Types struct WindowActionResult: Codable { let action: String let success: Bool let app_name: String let window_title: String? let new_bounds: WindowBounds? } // Using PeekabooCore.WindowListData for consistency

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