import AppKit
|
import Foundation
|
import AlignerCore
|
|
@MainActor
|
enum Round1ScreenshotProbe {
|
static func run(prettyPrinted: Bool) async throws {
|
let spaceEnumerator = try SkyLightSpaceEnumerator()
|
let displays = try spaceEnumerator.displays()
|
let fullscreenSpaceIDs = Set(
|
displays
|
.flatMap(\.spaces)
|
.filter { $0.type == .fullscreen }
|
.map(\.id)
|
)
|
let windowService = CGWindowAXWindowService(
|
spaceIDsByWindowIDProvider: spaceEnumerator.spaceIDsByWindowID(windowIDs:),
|
fullscreenSpaceIDs: fullscreenSpaceIDs
|
)
|
let windows = try windowService.allWindows()
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(displays: displays, windows: windows)
|
let logger = Round1ScreenshotProbeLogger()
|
let provider = ScreenCaptureKitScreenshotProvider(debugLogger: logger)
|
let session = ScreenshotCaptureSession()
|
let candidates = snapshot.appGroups.flatMap { group in
|
group.windows.map(\.window)
|
}
|
|
var sampleResults: [[String: Any]] = []
|
for kind in Round1ScreenshotProbeSampleKind.allCases {
|
guard let window = candidates.first(where: { kind.matches($0) }) else {
|
sampleResults.append([
|
"kind": kind.rawValue,
|
"status": "missingSampleWindow"
|
])
|
continue
|
}
|
|
let startedAt = Date()
|
let resolution = await provider.resolvedScreenshot(for: window, in: session)
|
let elapsedMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1000)
|
sampleResults.append(
|
sampleDictionary(
|
kind: kind,
|
window: window,
|
resolution: resolution,
|
elapsedMilliseconds: elapsedMilliseconds,
|
events: logger.events(for: window.id)
|
)
|
)
|
}
|
|
let json = try JSONSerialization.data(
|
withJSONObject: dictionary(
|
displays: displays,
|
windows: windows,
|
snapshot: snapshot,
|
samples: sampleResults
|
),
|
options: prettyPrinted ? [.prettyPrinted, .sortedKeys] : [.sortedKeys]
|
)
|
|
FileHandle.standardOutput.write(json)
|
FileHandle.standardOutput.write(Data("\n".utf8))
|
}
|
|
private static func dictionary(
|
displays: [AlignerDisplay],
|
windows: [AlignerWindow],
|
snapshot: QuickSwitchSnapshot,
|
samples: [[String: Any]]
|
) -> [String: Any] {
|
let missingCount = samples.filter { $0["status"] as? String == "missingSampleWindow" }.count
|
let realCount = samples.filter { $0["source"] as? String == "realScreenshot" }.count
|
let fallbackCount = samples.filter { $0["source"] as? String == "skeletonFallback" }.count
|
|
return [
|
"generatedAt": ISO8601DateFormatter().string(from: Date()),
|
"displayCount": displays.count,
|
"spaceCount": displays.flatMap(\.spaces).count,
|
"candidateWindowCount": windows.count,
|
"snapshotAppCount": snapshot.appGroups.count,
|
"snapshotWindowCount": snapshot.appGroups.flatMap(\.windows).count,
|
"sampleKinds": Round1ScreenshotProbeSampleKind.allCases.map(\.rawValue),
|
"missingSampleCount": missingCount,
|
"realScreenshotCount": realCount,
|
"fallbackCount": fallbackCount,
|
"completeSampleSet": missingCount == 0,
|
"samples": samples
|
]
|
}
|
|
private static func sampleDictionary(
|
kind: Round1ScreenshotProbeSampleKind,
|
window: AlignerWindow,
|
resolution: ScreenshotResolution,
|
elapsedMilliseconds: Int,
|
events: [ScreenshotDebugEvent]
|
) -> [String: Any] {
|
var result: [String: Any] = [
|
"kind": kind.rawValue,
|
"status": "sampled",
|
"elapsedMilliseconds": elapsedMilliseconds,
|
"window": windowDictionary(window),
|
"events": events.map(eventString)
|
]
|
|
switch resolution.source {
|
case .realScreenshot:
|
result["source"] = "realScreenshot"
|
case .skeletonFallback(let reason):
|
result["source"] = "skeletonFallback"
|
result["fallbackReason"] = fallbackReasonString(reason)
|
}
|
|
result["imageSize"] = [
|
"width": resolution.image.size.width,
|
"height": resolution.image.size.height
|
]
|
|
return result
|
}
|
|
private static func windowDictionary(_ window: AlignerWindow) -> [String: Any] {
|
var result = [
|
"id": window.id,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"isMinimized": window.isMinimized,
|
"isFullscreen": window.isFullscreen,
|
"spaceIDs": window.spaceIDs,
|
"app": [
|
"bundleIdentifier": window.app.bundleIdentifier,
|
"name": window.app.name,
|
"category": window.app.category.rawValue,
|
"processIdentifier": window.app.processIdentifier as Any
|
]
|
] as [String: Any]
|
if DevelopmentDiagnostics.includesSensitiveFields {
|
result["title"] = window.title
|
}
|
return result
|
}
|
|
private static func eventString(_ event: ScreenshotDebugEvent) -> String {
|
switch event {
|
case .captureStarted(let windowID):
|
return "captureStarted(\(windowID))"
|
case .captureSucceeded(let windowID):
|
return "captureSucceeded(\(windowID))"
|
case .captureFailed(let windowID, let reason):
|
return "captureFailed(\(windowID), \(fallbackReasonString(reason)))"
|
case .retrySkipped(let windowID, let failedAttempts):
|
return "retrySkipped(\(windowID), failedAttempts: \(failedAttempts))"
|
case .fallbackUsed(let windowID, let reason):
|
return "fallbackUsed(\(windowID), \(fallbackReasonString(reason)))"
|
}
|
}
|
|
private static func fallbackReasonString(_ reason: ScreenshotFallbackReason) -> String {
|
reason.diagnosticDescription
|
}
|
}
|
|
private enum Round1ScreenshotProbeSampleKind: String, CaseIterable {
|
case finder
|
case browser
|
case terminal
|
case document
|
case systemSettings
|
|
func matches(_ window: AlignerWindow) -> Bool {
|
guard !window.isMinimized else { return false }
|
|
let bundleIdentifier = window.app.bundleIdentifier
|
let appName = window.app.name.localizedLowercase
|
|
switch self {
|
case .finder:
|
return bundleIdentifier == "com.apple.finder" || window.app.category == .finder
|
case .browser:
|
return window.app.category == .browser
|
case .terminal:
|
return window.app.category == .terminal
|
case .document:
|
return window.app.category == .documentNotes
|
|| [
|
"com.apple.TextEdit",
|
"com.apple.Notes",
|
"com.apple.Preview",
|
"com.microsoft.Word",
|
"io.typora",
|
"md.obsidian"
|
].contains(bundleIdentifier)
|
case .systemSettings:
|
return bundleIdentifier == "com.apple.systempreferences"
|
|| appName == "system settings"
|
|| appName == "system preferences"
|
|| appName == "系统设置"
|
|| appName == "系统偏好设置"
|
}
|
}
|
}
|
|
@MainActor
|
private final class Round1ScreenshotProbeLogger: ScreenshotDebugLogging {
|
private var eventsByWindowID: [UInt32: [ScreenshotDebugEvent]] = [:]
|
|
func record(_ event: ScreenshotDebugEvent) {
|
eventsByWindowID[event.windowID, default: []].append(event)
|
}
|
|
func events(for windowID: UInt32) -> [ScreenshotDebugEvent] {
|
eventsByWindowID[windowID, default: []]
|
}
|
}
|
|
private extension ScreenshotDebugEvent {
|
var windowID: UInt32 {
|
switch self {
|
case .captureStarted(let windowID),
|
.captureSucceeded(let windowID),
|
.captureFailed(let windowID, _),
|
.retrySkipped(let windowID, _),
|
.fallbackUsed(let windowID, _):
|
return windowID
|
}
|
}
|
}
|
|
struct Round1ScreenshotProbeOptions {
|
let enabled: Bool
|
let prettyPrinted: Bool
|
|
static func parse(arguments: [String]) -> Round1ScreenshotProbeOptions {
|
Round1ScreenshotProbeOptions(
|
enabled: arguments.contains("--round01-screenshot-probe"),
|
prettyPrinted: arguments.contains("--round01-screenshot-probe-pretty")
|
)
|
}
|
}
|