mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-06 11:25:47 +00:00
- 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>
220 lines
6.8 KiB
Swift
220 lines
6.8 KiB
Swift
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
|
||
}
|