Peekaboo/peekaboo-cli/Sources/peekaboo/WindowManager.swift
Peter Steinberger c04b8e7af0 Migrate to Swift 6 with strict concurrency
- Update to swift-tools-version 6.0 and enable StrictConcurrency
- Make all data models and types Sendable for concurrency safety
- Migrate commands from ParsableCommand to AsyncParsableCommand
- Remove AsyncUtils.swift and synchronous bridging patterns
- Update WindowBounds property names to snake_case for consistency
- Ensure all error types conform to Sendable protocol
- Add comprehensive Swift 6 migration documentation

This migration enables full Swift 6 concurrency checking and data race
safety while maintaining backward compatibility with the existing API.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 11:23:10 +01:00

124 lines
4.4 KiB
Swift

import AppKit
import CoreGraphics
import Foundation
final class WindowManager: Sendable {
static func getWindowsForApp(pid: pid_t, includeOffScreen: Bool = false) throws(WindowError) -> [WindowData] {
// Logger.shared.debug("Getting windows for PID: \(pid)")
// In CI environment, return empty array to avoid accessing window server
if ProcessInfo.processInfo.environment["CI"] == "true" {
return []
}
let windowList = try fetchWindowList(includeOffScreen: includeOffScreen)
let windows = extractWindowsForPID(pid, from: windowList)
// Logger.shared.debug("Found \(windows.count) windows for PID \(pid)")
return windows.sorted { $0.windowIndex < $1.windowIndex }
}
private static func fetchWindowList(includeOffScreen: Bool) throws(WindowError) -> [[String: Any]] {
let options: CGWindowListOption = includeOffScreen
? [.excludeDesktopElements]
: [.excludeDesktopElements, .optionOnScreenOnly]
guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
throw WindowError.windowListFailed
}
return windowList
}
private static func extractWindowsForPID(_ pid: pid_t, from windowList: [[String: Any]]) -> [WindowData] {
var windows: [WindowData] = []
var windowIndex = 0
for windowInfo in windowList {
if let window = parseWindowInfo(windowInfo, targetPID: pid, index: windowIndex) {
windows.append(window)
windowIndex += 1
}
}
return windows
}
private static func parseWindowInfo(_ info: [String: Any], targetPID: pid_t, index: Int) -> WindowData? {
guard let windowPID = info[kCGWindowOwnerPID as String] as? Int32,
windowPID == targetPID,
let windowID = info[kCGWindowNumber as String] as? CGWindowID else {
return nil
}
let title = info[kCGWindowName as String] as? String ?? "Untitled"
let bounds = extractWindowBounds(from: info)
let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? true
return WindowData(
windowId: windowID,
title: title,
bounds: bounds,
isOnScreen: isOnScreen,
windowIndex: index
)
}
private static func extractWindowBounds(from windowInfo: [String: Any]) -> CGRect {
guard let boundsDict = windowInfo[kCGWindowBounds as String] as? [String: Any] else {
return .zero
}
let xCoordinate = boundsDict["X"] as? Double ?? 0
let yCoordinate = boundsDict["Y"] as? Double ?? 0
let width = boundsDict["Width"] as? Double ?? 0
let height = boundsDict["Height"] as? Double ?? 0
return CGRect(x: xCoordinate, y: yCoordinate, width: width, height: height)
}
static func getWindowsInfoForApp(
pid: pid_t,
includeOffScreen: Bool = false,
includeBounds: Bool = false,
includeIDs: Bool = false
) throws(WindowError) -> [WindowInfo] {
let windowDataArray = try getWindowsForApp(pid: pid, includeOffScreen: includeOffScreen)
return windowDataArray.map { windowData in
WindowInfo(
window_title: windowData.title,
window_id: includeIDs ? windowData.windowId : nil,
window_index: windowData.windowIndex,
bounds: includeBounds ? WindowBounds(
x_coordinate: Int(windowData.bounds.origin.x),
y_coordinate: Int(windowData.bounds.origin.y),
width: Int(windowData.bounds.size.width),
height: Int(windowData.bounds.size.height)
) : nil,
is_on_screen: includeOffScreen ? windowData.isOnScreen : nil
)
}
}
}
// Extension to add the getWindowsForApp function to ImageCommand
extension ImageCommand {
func getWindowsForApp(pid: pid_t) throws(WindowError) -> [WindowData] {
try WindowManager.getWindowsForApp(pid: pid)
}
}
enum WindowError: Error, LocalizedError, Sendable {
case windowListFailed
case noWindowsFound
var errorDescription: String? {
switch self {
case .windowListFailed:
"Failed to get window list from system"
case .noWindowsFound:
"No windows found for the specified application"
}
}
}