Skip to main content
Glama

Peekaboo MCP

by steipete
SpaceCommand.swiftβ€’12.5 kB
import AppKit import ArgumentParser import Foundation import PeekabooCore /// Manage macOS Spaces (virtual desktops) struct SpaceCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "space", abstract: "Manage macOS Spaces (virtual desktops)", discussion: """ SYNOPSIS: peekaboo space SUBCOMMAND [OPTIONS] DESCRIPTION: Provides Space (virtual desktop) management capabilities including listing Spaces, switching between them, and moving windows. EXAMPLES: # List all Spaces peekaboo space list # Switch to Space 2 peekaboo space switch --to 2 # Move window to Space 3 peekaboo space move-window --app Safari --to 3 # Move window to current Space peekaboo space move-window --app Terminal --to-current SUBCOMMANDS: list List all Spaces and their windows switch Switch to a different Space move-window Move a window to a different Space NOTE: Space management uses private macOS APIs that may change between macOS versions. Some features may not work on all systems. """, subcommands: [ ListSubcommand.self, SwitchSubcommand.self, MoveWindowSubcommand.self, ] ) } // MARK: - List Spaces struct ListSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "list", abstract: "List all Spaces and their windows" ) @Flag(name: .long, help: "Include detailed window information") var detailed = false @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false @MainActor func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) let spaceService = SpaceManagementService() let spaces = spaceService.getAllSpaces() if self.jsonOutput { let data = SpaceListData( spaces: spaces.map { space in SpaceData( id: space.id, type: space.type.rawValue, is_active: space.isActive, display_id: space.displayID ) } ) outputSuccessCodable(data: data) } else { print("Spaces:") // If detailed, collect all windows and their Space assignments var windowsBySpace: [UInt64: [(app: String, window: ServiceWindowInfo)]] = [:] if self.detailed { // Get all running applications let appService = ApplicationService() let appListResult = try await appService.listApplications() // Iterate through all applications to get their windows for app in appListResult.data.applications { // Skip apps without windows guard app.windowCount > 0 else { continue } do { // Get windows for this application let windowsResult = try await appService.listWindows(for: app.name) // For each window, find which Space it's on for window in windowsResult.data.windows { let windowID = CGWindowID(window.windowID) let windowSpaces = spaceService.getSpacesForWindow(windowID: windowID) // Add window to each Space it appears on for space in windowSpaces { if windowsBySpace[space.id] == nil { windowsBySpace[space.id] = [] } windowsBySpace[space.id]?.append((app: app.name, window: window)) } } } catch { // Skip applications we can't access continue } } } // Display Spaces with their windows for (index, space) in spaces.enumerated() { let marker = space.isActive ? "β†’" : " " let displayInfo = space.displayID.map { " (Display \($0))" } ?? "" print("\(marker) Space \(index + 1) [ID: \(space.id), Type: \(space.type.rawValue)\(displayInfo)]") if self.detailed { // Display windows for this Space if let windows = windowsBySpace[space.id], !windows.isEmpty { for (app, window) in windows { let title = window.title.isEmpty ? "[Untitled]" : window.title let minimized = window.isMinimized ? " [MINIMIZED]" : "" print(" β€’ \(app): \(title)\(minimized)") } } else { print(" (No windows)") } } } if spaces.isEmpty { print("No Spaces found (this may indicate an API issue)") } } } } // MARK: - Switch Space struct SwitchSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable { static let configuration = CommandConfiguration( commandName: "switch", abstract: "Switch to a different Space" ) @Option(name: .long, help: "Space number to switch to (1-based)") var to: Int @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { let spaceService = await MainActor.run { SpaceManagementService() } let spaces = await MainActor.run { spaceService.getAllSpaces() } // Convert 1-based index to actual Space guard self.to > 0 && self.to <= spaces.count else { throw ValidationError("Invalid Space number. Available: 1-\(spaces.count)") } let targetSpace = spaces[self.to - 1] try await spaceService.switchToSpace(targetSpace.id) if self.jsonOutput { let data = SpaceActionResult( action: "switch", success: true, space_id: targetSpace.id, space_number: self.to ) outputSuccessCodable(data: data) } else { print("βœ“ Switched to Space \(self.to)") } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Move Window to Space struct MoveWindowSubcommand: AsyncParsableCommand, ErrorHandlingCommand, OutputFormattable, ApplicationResolvable { static let configuration = CommandConfiguration( commandName: "move-window", abstract: "Move a window to a different Space" ) @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? @Option(name: .long, help: "Space number to move window to (1-based)") var to: Int? @Flag(name: .long, help: "Move window to current Space") var toCurrent = false @Flag(name: .long, help: "Switch to the target Space after moving") var follow = false @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false func run() async throws { Logger.shared.setJsonOutputMode(self.jsonOutput) do { // Validate inputs let appIdentifier = try self.resolveApplicationIdentifier() guard self.to != nil || self.toCurrent else { throw ValidationError("Must specify either --to or --to-current") } guard !(self.to != nil && self.toCurrent) else { throw ValidationError("Cannot specify both --to and --to-current") } // Create window identification options var windowOptions = WindowIdentificationOptions() windowOptions.app = appIdentifier windowOptions.windowTitle = self.windowTitle windowOptions.windowIndex = self.windowIndex // Get window info let target = try windowOptions.toWindowTarget() let windows = try await PeekabooServices.shared.windows.listWindows(target: target) let windowInfo = windowOptions.selectWindow(from: windows) guard let info = windowInfo else { throw NotFoundError.window(app: appIdentifier) } let windowID = CGWindowID(info.windowID) let spaceService = await MainActor.run { SpaceManagementService() } if self.toCurrent { // Move to current Space try await MainActor.run { try spaceService.moveWindowToCurrentSpace(windowID: windowID) } if self.jsonOutput { let data = WindowSpaceActionResult( action: "move-window", success: true, window_id: windowID, window_title: info.title, space_id: nil, space_number: nil, moved_to_current: true, followed: nil ) outputSuccessCodable(data: data) } else { print("βœ“ Moved window '\(info.title)' to current Space") } } else if let spaceNum = self.to { // Move to specific Space let spaces = await MainActor.run { spaceService.getAllSpaces() } guard spaceNum > 0 && spaceNum <= spaces.count else { throw ValidationError("Invalid Space number. Available: 1-\(spaces.count)") } let targetSpace = spaces[spaceNum - 1] try await MainActor.run { try spaceService.moveWindowToSpace(windowID: windowID, spaceID: targetSpace.id) } if self.follow { try await spaceService.switchToSpace(targetSpace.id) } if self.jsonOutput { let data = WindowSpaceActionResult( action: "move-window", success: true, window_id: windowID, window_title: info.title, space_id: targetSpace.id, space_number: spaceNum, moved_to_current: false, followed: self.follow ) outputSuccessCodable(data: data) } else { var message = "βœ“ Moved window '\(info.title)' to Space \(spaceNum)" if self.follow { message += " (and switched to it)" } print(message) } } } catch { handleError(error) throw ExitCode(1) } } } // MARK: - Response Types struct SpaceListData: Codable { let spaces: [SpaceData] } struct SpaceData: Codable { let id: UInt64 let type: String let is_active: Bool let display_id: CGDirectDisplayID? } struct SpaceActionResult: Codable { let action: String let success: Bool let space_id: UInt64 let space_number: Int } struct WindowSpaceActionResult: Codable { let action: String let success: Bool let window_id: CGWindowID let window_title: String let space_id: UInt64? let space_number: Int? let moved_to_current: Bool? let followed: Bool? }

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