mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-12 12:25:50 +00:00
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
6.6 KiB
Swift
212 lines
6.6 KiB
Swift
import ArgumentParser
|
|
import Foundation
|
|
|
|
// MARK: - Image Capture Models
|
|
|
|
struct SavedFile: Codable, Sendable {
|
|
let path: String
|
|
let item_label: String?
|
|
let window_title: String?
|
|
let window_id: UInt32?
|
|
let window_index: Int?
|
|
let mime_type: String
|
|
}
|
|
|
|
struct ImageCaptureData: Codable, Sendable {
|
|
let saved_files: [SavedFile]
|
|
}
|
|
|
|
enum CaptureMode: String, CaseIterable, ExpressibleByArgument, Sendable {
|
|
case screen
|
|
case window
|
|
case multi
|
|
case frontmost
|
|
}
|
|
|
|
enum ImageFormat: String, CaseIterable, ExpressibleByArgument, Sendable {
|
|
case png
|
|
case jpg
|
|
}
|
|
|
|
enum CaptureFocus: String, CaseIterable, ExpressibleByArgument, Sendable {
|
|
case background
|
|
case auto
|
|
case foreground
|
|
}
|
|
|
|
// MARK: - Application & Window Models
|
|
|
|
struct ApplicationInfo: Codable, Sendable {
|
|
let app_name: String
|
|
let bundle_id: String
|
|
let pid: Int32
|
|
let is_active: Bool
|
|
let window_count: Int
|
|
}
|
|
|
|
struct ApplicationListData: Codable, Sendable {
|
|
let applications: [ApplicationInfo]
|
|
}
|
|
|
|
struct WindowInfo: Codable, Sendable {
|
|
let window_title: String
|
|
let window_id: UInt32?
|
|
let window_index: Int?
|
|
let bounds: WindowBounds?
|
|
let is_on_screen: Bool?
|
|
}
|
|
|
|
struct WindowBounds: Codable, Sendable {
|
|
let x_coordinate: Int
|
|
let y_coordinate: Int
|
|
let width: Int
|
|
let height: Int
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case x_coordinate
|
|
case y_coordinate
|
|
case width
|
|
case height
|
|
}
|
|
}
|
|
|
|
struct TargetApplicationInfo: Codable, Sendable {
|
|
let app_name: String
|
|
let bundle_id: String?
|
|
let pid: Int32
|
|
}
|
|
|
|
struct WindowListData: Codable, Sendable {
|
|
let windows: [WindowInfo]
|
|
let target_application_info: TargetApplicationInfo
|
|
}
|
|
|
|
// MARK: - Window Specifier
|
|
|
|
enum WindowSpecifier: Sendable {
|
|
case title(String)
|
|
case index(Int)
|
|
}
|
|
|
|
// MARK: - Window Details Options
|
|
|
|
enum WindowDetailOption: String, CaseIterable, Sendable {
|
|
case off_screen
|
|
case bounds
|
|
case ids
|
|
}
|
|
|
|
// MARK: - Window Management
|
|
|
|
struct WindowData: Sendable {
|
|
let windowId: UInt32
|
|
let title: String
|
|
let bounds: CGRect
|
|
let isOnScreen: Bool
|
|
let windowIndex: Int
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
enum CaptureError: Error, LocalizedError, Sendable {
|
|
case noDisplaysAvailable
|
|
case screenRecordingPermissionDenied
|
|
case accessibilityPermissionDenied
|
|
case invalidDisplayID
|
|
case captureCreationFailed(Error?)
|
|
case windowNotFound
|
|
case windowTitleNotFound(String, String, String) // searchTerm, appName, availableTitles
|
|
case windowCaptureFailed(Error?)
|
|
case fileWriteError(String, Error?)
|
|
case appNotFound(String)
|
|
case invalidWindowIndex(Int)
|
|
case invalidArgument(String)
|
|
case unknownError(String)
|
|
case noWindowsFound(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noDisplaysAvailable:
|
|
return "No displays available for capture."
|
|
case .screenRecordingPermissionDenied:
|
|
return "Screen recording permission is required. " +
|
|
"Please grant it in System Settings > Privacy & Security > Screen Recording."
|
|
case .accessibilityPermissionDenied:
|
|
return "Accessibility permission is required for some operations. " +
|
|
"Please grant it in System Settings > Privacy & Security > Accessibility."
|
|
case .invalidDisplayID:
|
|
return "Invalid display ID provided."
|
|
case let .captureCreationFailed(underlyingError):
|
|
var message = "Failed to create the screen capture."
|
|
if let error = underlyingError {
|
|
message += " \(error.localizedDescription)"
|
|
}
|
|
return message
|
|
case .windowNotFound:
|
|
return "The specified window could not be found."
|
|
case let .windowTitleNotFound(searchTerm, appName, availableTitles):
|
|
var message = "Window with title containing '\(searchTerm)' not found in \(appName)."
|
|
if !availableTitles.isEmpty {
|
|
message += " Available windows: \(availableTitles)."
|
|
}
|
|
message +=
|
|
" Note: For URLs, try without the protocol (e.g., 'example.com:8080' instead of 'http://example.com:8080')."
|
|
return message
|
|
case let .windowCaptureFailed(underlyingError):
|
|
var message = "Failed to capture the specified window."
|
|
if let error = underlyingError {
|
|
message += " \(error.localizedDescription)"
|
|
}
|
|
return message
|
|
case let .fileWriteError(path, underlyingError):
|
|
var message = "Failed to write capture file to path: \(path)."
|
|
|
|
if let error = underlyingError {
|
|
let errorString = error.localizedDescription
|
|
if errorString.lowercased().contains("permission") {
|
|
message += " Permission denied - check that the directory is " +
|
|
"writable and the application has necessary permissions."
|
|
} else if errorString.lowercased().contains("no such file") {
|
|
message += " Directory does not exist - ensure the parent directory exists."
|
|
} else if errorString.lowercased().contains("no space") {
|
|
message += " Insufficient disk space available."
|
|
} else {
|
|
message += " \(errorString)"
|
|
}
|
|
} else {
|
|
message += " This may be due to insufficient permissions, missing directory, or disk space issues."
|
|
}
|
|
|
|
return message
|
|
case let .appNotFound(identifier):
|
|
return "Application with identifier '\(identifier)' not found or is not running."
|
|
case let .invalidWindowIndex(index):
|
|
return "Invalid window index: \(index)."
|
|
case let .invalidArgument(message):
|
|
return "Invalid argument: \(message)"
|
|
case let .unknownError(message):
|
|
return "An unexpected error occurred: \(message)"
|
|
case let .noWindowsFound(appName):
|
|
return "The '\(appName)' process is running, but no capturable windows were found."
|
|
}
|
|
}
|
|
|
|
var exitCode: Int32 {
|
|
switch self {
|
|
case .noDisplaysAvailable: 10
|
|
case .screenRecordingPermissionDenied: 11
|
|
case .accessibilityPermissionDenied: 12
|
|
case .invalidDisplayID: 13
|
|
case .captureCreationFailed: 14
|
|
case .windowNotFound: 15
|
|
case .windowTitleNotFound: 21
|
|
case .windowCaptureFailed: 16
|
|
case .fileWriteError: 17
|
|
case .appNotFound: 18
|
|
case .invalidWindowIndex: 19
|
|
case .invalidArgument: 20
|
|
case .unknownError: 1
|
|
case .noWindowsFound: 7
|
|
}
|
|
}
|
|
}
|