Peekaboo/peekaboo-cli/Sources/peekaboo/ListCommand.swift
Peter Steinberger de5a0cb97e 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>
2025-06-07 22:51:49 +01:00

220 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AppKit
import ArgumentParser
import Foundation
struct ListCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List running applications or windows",
subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self],
defaultSubcommand: AppsSubcommand.self
)
}
struct AppsSubcommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "apps",
abstract: "List all running applications"
)
@Flag(name: .long, help: "Output results in JSON format")
var jsonOutput = false
func run() throws {
Logger.shared.setJsonOutputMode(jsonOutput)
do {
try PermissionsChecker.requireScreenRecordingPermission()
let applications = ApplicationFinder.getAllRunningApplications()
let data = ApplicationListData(applications: applications)
if jsonOutput {
outputSuccess(data: data)
} else {
printApplicationList(applications)
}
} catch {
Logger.shared.error("Failed to list applications: \(error)")
if jsonOutput {
outputError(message: error.localizedDescription, code: .INTERNAL_SWIFT_ERROR)
} else {
fputs("Error: \(error.localizedDescription)\n", stderr)
}
throw ExitCode.failure
}
}
private func printApplicationList(_ applications: [ApplicationInfo]) {
print("Running Applications (\(applications.count)):")
print()
for (index, app) in applications.enumerated() {
print("\(index + 1). \(app.app_name)")
print(" Bundle ID: \(app.bundle_id)")
print(" PID: \(app.pid)")
print(" Status: \(app.is_active ? "Active" : "Background")")
print(" Windows: \(app.window_count)")
print()
}
}
}
struct WindowsSubcommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "windows",
abstract: "List windows for a specific application"
)
@Option(name: .long, help: "Target application identifier")
var app: String
@Option(name: .long, help: "Include additional window details (comma-separated: off_screen,bounds,ids)")
var includeDetails: String?
@Flag(name: .long, help: "Output results in JSON format")
var jsonOutput = false
func run() throws {
Logger.shared.setJsonOutputMode(jsonOutput)
do {
try PermissionsChecker.requireScreenRecordingPermission()
// Find the target application
let targetApp = try ApplicationFinder.findApplication(identifier: app)
// Parse include details options
let detailOptions = parseIncludeDetails()
// Get windows for the app
let windows = try WindowManager.getWindowsInfoForApp(
pid: targetApp.processIdentifier,
includeOffScreen: detailOptions.contains(.off_screen),
includeBounds: detailOptions.contains(.bounds),
includeIDs: detailOptions.contains(.ids)
)
let targetAppInfo = TargetApplicationInfo(
app_name: targetApp.localizedName ?? "Unknown",
bundle_id: targetApp.bundleIdentifier,
pid: targetApp.processIdentifier
)
let data = WindowListData(
windows: windows,
target_application_info: targetAppInfo
)
if jsonOutput {
outputSuccess(data: data)
} else {
printWindowList(data)
}
} catch {
Logger.shared.error("Failed to list windows: \(error)")
if jsonOutput {
outputError(message: error.localizedDescription, code: .INTERNAL_SWIFT_ERROR)
} else {
fputs("Error: \(error.localizedDescription)\n", stderr)
}
throw ExitCode.failure
}
}
private func parseIncludeDetails() -> Set<WindowDetailOption> {
guard let detailsString = includeDetails else {
return []
}
let components = detailsString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
var options: Set<WindowDetailOption> = []
for component in components {
if let option = WindowDetailOption(rawValue: component) {
options.insert(option)
}
}
return options
}
private func printWindowList(_ data: WindowListData) {
let app = data.target_application_info
let windows = data.windows
print("Windows for \(app.app_name)")
if let bundleId = app.bundle_id {
print("Bundle ID: \(bundleId)")
}
print("PID: \(app.pid)")
print("Total Windows: \(windows.count)")
print()
if windows.isEmpty {
print("No windows found.")
return
}
for (index, window) in windows.enumerated() {
print("\(index + 1). \"\(window.window_title)\"")
if let windowId = window.window_id {
print(" Window ID: \(windowId)")
}
if let isOnScreen = window.is_on_screen {
print(" On Screen: \(isOnScreen ? "Yes" : "No")")
}
if let bounds = window.bounds {
print(" Bounds: (\(bounds.xCoordinate), \(bounds.yCoordinate)) \(bounds.width)×\(bounds.height)")
}
print()
}
}
}
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
}