diff --git a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift index 2a6159d..ec35e64 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift @@ -322,38 +322,55 @@ struct ImageCommand: ParsableCommand { if let error = captureError { throw error } + } catch let error as CaptureError { + // Re-throw CaptureError as-is + throw error } catch { + // Check if this is a permission error from ScreenCaptureKit + let errorString = error.localizedDescription.lowercased() + if errorString.contains("screen recording") || errorString.contains("permission") { + throw CaptureError.screenRecordingPermissionDenied + } throw CaptureError.captureCreationFailed } } private func captureDisplayWithScreenCaptureKit(_ displayID: CGDirectDisplayID, to path: String) async throws { - // Get available content - let availableContent = try await SCShareableContent.current + do { + // Get available content + let availableContent = try await SCShareableContent.current - // Find the display by ID - guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else { - throw CaptureError.captureCreationFailed + // Find the display by ID + guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else { + throw CaptureError.captureCreationFailed + } + + // Create content filter for the entire display + let filter = SCContentFilter(display: scDisplay, excludingWindows: []) + + // Configure capture settings + let configuration = SCStreamConfiguration() + configuration.width = scDisplay.width + configuration.height = scDisplay.height + configuration.backgroundColor = .black + configuration.shouldBeOpaque = true + configuration.showsCursor = true + + // Capture the image + let image = try await SCScreenshotManager.captureImage( + contentFilter: filter, + configuration: configuration + ) + + try saveImage(image, to: path) + } catch { + // Check if this is a permission error + let errorString = error.localizedDescription.lowercased() + if errorString.contains("screen recording") || errorString.contains("permission") { + throw CaptureError.screenRecordingPermissionDenied + } + throw error } - - // Create content filter for the entire display - let filter = SCContentFilter(display: scDisplay, excludingWindows: []) - - // Configure capture settings - let configuration = SCStreamConfiguration() - configuration.width = scDisplay.width - configuration.height = scDisplay.height - configuration.backgroundColor = .black - configuration.shouldBeOpaque = true - configuration.showsCursor = true - - // Capture the image - let image = try await SCScreenshotManager.captureImage( - contentFilter: filter, - configuration: configuration - ) - - try saveImage(image, to: path) } private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) { @@ -375,38 +392,55 @@ struct ImageCommand: ParsableCommand { if let error = captureError { throw error } + } catch let error as CaptureError { + // Re-throw CaptureError as-is + throw error } catch { + // Check if this is a permission error from ScreenCaptureKit + let errorString = error.localizedDescription.lowercased() + if errorString.contains("screen recording") || errorString.contains("permission") { + throw CaptureError.screenRecordingPermissionDenied + } throw CaptureError.windowCaptureFailed } } private func captureWindowWithScreenCaptureKit(_ window: WindowData, to path: String) async throws { - // Get available content - let availableContent = try await SCShareableContent.current + do { + // Get available content + let availableContent = try await SCShareableContent.current - // Find the window by ID - guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else { - throw CaptureError.windowNotFound + // Find the window by ID + guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else { + throw CaptureError.windowNotFound + } + + // Create content filter for the specific window + let filter = SCContentFilter(desktopIndependentWindow: scWindow) + + // Configure capture settings + let configuration = SCStreamConfiguration() + configuration.width = Int(window.bounds.width) + configuration.height = Int(window.bounds.height) + configuration.backgroundColor = .clear + configuration.shouldBeOpaque = true + configuration.showsCursor = false + + // Capture the image + let image = try await SCScreenshotManager.captureImage( + contentFilter: filter, + configuration: configuration + ) + + try saveImage(image, to: path) + } catch { + // Check if this is a permission error + let errorString = error.localizedDescription.lowercased() + if errorString.contains("screen recording") || errorString.contains("permission") { + throw CaptureError.screenRecordingPermissionDenied + } + throw error } - - // Create content filter for the specific window - let filter = SCContentFilter(desktopIndependentWindow: scWindow) - - // Configure capture settings - let configuration = SCStreamConfiguration() - configuration.width = Int(window.bounds.width) - configuration.height = Int(window.bounds.height) - configuration.backgroundColor = .clear - configuration.shouldBeOpaque = true - configuration.showsCursor = false - - // Capture the image - let image = try await SCScreenshotManager.captureImage( - contentFilter: filter, - configuration: configuration - ) - - try saveImage(image, to: path) } private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) { diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index 0f5ce40..f8e0eaa 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -6,7 +6,7 @@ struct ListCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "list", abstract: "List running applications or windows", - subcommands: [AppsSubcommand.self, WindowsSubcommand.self], + subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self], defaultSubcommand: AppsSubcommand.self ) } @@ -177,3 +177,44 @@ struct WindowsSubcommand: ParsableCommand { } } } + +struct ServerStatusSubcommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "server_status", + abstract: "Check server permissions status" + ) + + @Flag(name: .long, help: "Output results in JSON format") + var jsonOutput = false + + func run() throws { + Logger.shared.setJsonOutputMode(jsonOutput) + + let screenRecording = PermissionsChecker.checkScreenRecordingPermission() + let accessibility = PermissionsChecker.checkAccessibilityPermission() + + let permissions = ServerPermissions( + screen_recording: screenRecording, + accessibility: accessibility + ) + + let data = ServerStatusData(permissions: permissions) + + if jsonOutput { + outputSuccess(data: data) + } else { + print("Server Permissions Status:") + print(" Screen Recording: \(screenRecording ? "✅ Granted" : "❌ Not granted")") + print(" Accessibility: \(accessibility ? "✅ Granted" : "❌ Not granted")") + } + } +} + +struct ServerPermissions: Codable { + let screen_recording: Bool + let accessibility: Bool +} + +struct ServerStatusData: Codable { + let permissions: ServerPermissions +} diff --git a/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift b/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift index 8493af4..b9736a5 100644 --- a/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift +++ b/peekaboo-cli/Sources/peekaboo/PermissionsChecker.swift @@ -1,24 +1,29 @@ import AVFoundation import CoreGraphics import Foundation +import ScreenCaptureKit class PermissionsChecker { static func checkScreenRecordingPermission() -> Bool { - // Check if we can capture screen content by trying to get display bounds - var displayCount: UInt32 = 0 - let result = CGGetActiveDisplayList(0, nil, &displayCount) - if result != .success || displayCount == 0 { - return false + // ScreenCaptureKit requires screen recording permission + // We check by attempting to get shareable content + let semaphore = DispatchSemaphore(value: 0) + var hasPermission = false + + Task { + do { + // This will fail if we don't have screen recording permission + _ = try await SCShareableContent.current + hasPermission = true + } catch { + // If we get an error, we don't have permission + hasPermission = false + } + semaphore.signal() } - - // Try to capture a small image to test permissions - guard let mainDisplayID = CGMainDisplayID() as CGDirectDisplayID? else { - return false - } - - // Test by trying to get display bounds - this requires screen recording permission - let bounds = CGDisplayBounds(mainDisplayID) - return bounds != CGRect.zero + + semaphore.wait() + return hasPermission } static func checkAccessibilityPermission() -> Bool {