Fix Screen Recording permission detection and improve error reporting

- Replace broken CGDisplayBounds check with ScreenCaptureKit API
- Add proper error handling to detect permission-related failures
- Add server_status subcommand to expose permission status via JSON
- Ensure users get clear error messages when permissions are missing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-06-07 22:51:49 +01:00
parent a491adbdf1
commit de5a0cb97e
3 changed files with 143 additions and 63 deletions

View file

@ -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) {

View file

@ -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
}

View file

@ -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 {