Files
terebi/Checks/app-startup.swift
T

1113 lines
56 KiB
Swift

import AppKit
import Combine
import Darwin
import MediaPlayer
import ObjectiveC
@_cdecl("terebi_app_main")
@MainActor
public func checkAppStartup() {
if let index = CommandLine.arguments.firstIndex(of: "--check-startup-error") {
StartupErrorCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let index = CommandLine.arguments.firstIndex(of: "--check-overlay-hover") {
OverlayHoverCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let index = CommandLine.arguments.firstIndex(of: "--check-window-drag") {
WindowDragCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let index = CommandLine.arguments.firstIndex(of: "--check-queue-drag") {
QueueDragCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let index = CommandLine.arguments.firstIndex(of: "--check-window-layout") {
WindowLayoutCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let configuration = Bundle.main.url(forResource: "cli-check", withExtension: "json") {
CLIInstanceCheck.run(configuration)
return
}
AppStartupCheck.run()
}
@MainActor
private enum StartupErrorCheck {
static var timer: Timer?
static func run(result: URL) {
let deadline = ContinuousClock.now.advanced(by: .seconds(15))
let observer = Timer(timeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated {
guard let window = NSApp.modalWindow, let content = window.contentView else {
precondition(ContinuousClock.now < deadline, "Startup failure must present a native modal alert")
return
}
@MainActor
func descendants(_ view: NSView) -> [NSView] {
[view] + view.subviews.flatMap { descendants($0) }
}
let views = descendants(content)
let text = views.compactMap { ($0 as? NSTextField)?.stringValue }.filter { !$0.isEmpty }
let button = views.compactMap { $0 as? NSButton }.first { $0.title == "OK" || $0.title == "Quit" }
let focused = NSApp.isActive && window.isKeyWindow
guard focused || ContinuousClock.now >= deadline else { return }
let report: [String: Any] = [
"passed": focused && window.isVisible && button?.isEnabled == true,
"active": NSApp.isActive, "keyWindow": window.isKeyWindow,
"visible": window.isVisible, "text": text,
]
do {
try JSONSerialization.data(withJSONObject: report).write(to: result, options: .atomic)
} catch {
fatalError("Could not report native startup alert: \(error)")
}
timer?.invalidate()
guard let button else { fatalError("Startup alert must have a dismissal button") }
button.performClick(nil)
}
}
timer = observer
RunLoop.main.add(observer, forMode: .modalPanel)
terebiAppMain()
fatalError("Invalid startup input must not start the player")
}
}
@MainActor
private enum OverlayHoverCheck {
static var timer: Timer?
static var observation: AnyCancellable?
static var resultURL: URL!
static var step = 0
static var sample = 0
static var hidden = 0
static var savedPointer = NSPoint.zero
static let names = ["video", "title", "playback controls", "top-right controls",
"queue controls", "window exit", "window reentry"]
// Infrastructure watchdog only; verdicts use native hover events and visibility changes.
static let deadline = ContinuousClock.now.advanced(by: .seconds(20))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
savedPointer = NSEvent.mouseLocation
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
terebiAppMain()
}
static func finish(_ error: String = "") {
timer?.invalidate()
observation = nil
CGWarpMouseCursorPosition(CGPoint(x: savedPointer.x, y: NSScreen.screens[0].frame.maxY - savedPointer.y))
let report: [String: Any] = ["passed": error.isEmpty, "error": error, "steps": step]
try! JSONSerialization.data(withJSONObject: report).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func advance() {
guard ContinuousClock.now < deadline else { finish("Overlay hover timed out"); return }
let model = PlayerModel.shared
guard model.canTogglePlayback, model.state.has_video,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }),
let content = window.contentView else { return }
if observation == nil {
model.setVolume(0)
model.setPlayback(playing: false)
window.makeKeyAndOrderFront(nil)
window.makeMain()
NSApp.activate(ignoringOtherApps: true)
observation = model.$controlsVisible.removeDuplicates().dropFirst().sink { visible in
if !visible { hidden += 1 }
}
}
if step == names.count { finish(); return }
if step == 4 && !model.showsQueue { model.showsQueue = true; return }
guard !model.isQueueTransitioning else { return }
if sample == 0 {
let w = content.bounds.width
let h = content.bounds.height
let points = [NSPoint(x: w / 2, y: h / 2), NSPoint(x: 150, y: h - 16),
NSPoint(x: w / 2, y: 64), NSPoint(x: w - 46, y: h - 16),
NSPoint(x: w - 46, y: h - 16), NSPoint(x: -20, y: h / 2),
NSPoint(x: w - 46, y: h - 16)]
hidden = 0
let screen = window.convertPoint(toScreen: points[step])
CGWarpMouseCursorPosition(CGPoint(x: screen.x, y: NSScreen.screens[0].frame.maxY - screen.y))
let event = NSEvent.mouseEvent(with: .mouseMoved, location: points[step], modifierFlags: [],
timestamp: 0, windowNumber: window.windowNumber, context: nil,
eventNumber: step, clickCount: 0, pressure: 0)!
NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false)
}
// Let native tracking and SwiftUI updates settle; no auto-hide timer is under test.
sample += 1
if sample == 20 {
let outside = step == 5
guard model.controlsVisible != outside, outside || hidden == 0 else {
finish("\(names[step]): visible=\(model.controlsVisible), hidden transitions=\(hidden)")
return
}
sample = 0
step += 1
}
}
}
@MainActor
private enum WindowDragCheck {
static var timer: Timer?
static var resultURL: URL!
static var phase = 0
static var probe = 0
static var drags = 0
static var previousDrags = 0
static var start = NSPoint.zero
static var implementations: [(Method, IMP)] = []
static var target: NSWindow?
static var dragDepth = 0
static var urlWindowFrame = NSRect.zero
static let urlDraft = "https://www.youtube.com/watch?v=aqz-KE-bpKQ"
static let names = ["video", "title", "playback padding", "play button", "volume slider",
"start whitespace", "start button", "start artwork", "URL text field",
"queue whitespace", "queue footer", "queue row", "queue button",
"error backdrop", "error button", "audio artwork",
"compact video", "compact padding", "compact button", "hidden controls"]
static let controls: Set<Int> = [3, 4, 6, 8, 11, 12, 14, 18]
// Infrastructure timeout only; verdicts depend on input routing, not elapsed time.
static let deadline = ContinuousClock.now.advanced(by: .seconds(25))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
if CommandLine.arguments.contains("--queue-add-only") { probe = 12 }
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
RunLoop.main.add(timer!, forMode: .common)
RunLoop.main.add(timer!, forMode: .modalPanel)
terebiAppMain()
}
static func finish(_ error: String? = nil) {
timer?.invalidate()
for (method, original) in implementations {
imp_removeBlock(method_setImplementation(method, original))
}
try! JSONSerialization.data(withJSONObject: [
"passed": error == nil, "error": error ?? "", "probes": probe, "drags": drags,
]).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func nativeView(in view: NSView, matching predicate: (NSView) -> Bool) -> NSView? {
if predicate(view) { return view }
for child in view.subviews {
if let found = nativeView(in: child, matching: predicate) { return found }
}
return nil
}
static func clickStartURL(in window: NSWindow) {
if phase != 0 {
let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command, .shift],
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
context: nil, characters: "O", charactersIgnoringModifiers: "o", isARepeat: false, keyCode: 31)!
NSApp.postEvent(event, atStart: false)
return
}
for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] {
let event = NSEvent.mouseEvent(with: type, location: NSPoint(x: 400, y: 272),
modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: window.windowNumber, context: nil,
eventNumber: probe * 100 + phase * 2 + (type == .leftMouseDown ? 0 : 1),
clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0)!
NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false)
}
}
static func clickQueueAdd(in window: NSWindow, link: Bool) {
for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] {
let event = NSEvent.mouseEvent(with: type,
location: NSPoint(x: window.contentView!.bounds.width - (link ? 163 : 183), y: 18),
modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: window.windowNumber, context: nil,
eventNumber: 1000 + phase * 2 + (type == .leftMouseDown ? 0 : 1),
clickCount: 1, pressure: type == .leftMouseDown ? 1 : 0)!
NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false)
}
}
static func point(in window: NSWindow) -> NSPoint? {
let content = window.contentView!
let w = content.bounds.width
let h = content.bounds.height
let view: NSView?
switch probe {
case 0, 7: return NSPoint(x: 100, y: 160)
case 1: return NSPoint(x: 150, y: h - 16)
case 2: return NSPoint(x: w / 2 - 210, y: 48)
case 3: return NSPoint(x: w / 2, y: 64)
case 4: view = nativeView(in: content) { ($0 as? NSSlider)?.accessibilityLabel() == "Volume" }
case 5: return NSPoint(x: 600, y: 160)
case 6: return NSPoint(x: 400, y: 272)
case 8: view = nativeView(in: content) { ($0 as? NSTextField)?.isEditable == true }
case 9: return NSPoint(x: w - 130, y: h / 2)
case 10: return NSPoint(x: w - 160, y: 18)
case 11: return NSPoint(x: w - 130, y: h - 80)
case 12: return NSPoint(x: w - 183, y: 18)
case 13: return NSPoint(x: 100, y: h / 2)
case 14: return NSPoint(x: w / 2, y: h / 2 - 51.5)
case 15, 19: return NSPoint(x: w / 2, y: h / 2)
case 16: return NSPoint(x: 100, y: 100)
case 17: return NSPoint(x: w / 2 - 77, y: 28)
case 18: return NSPoint(x: w / 2 - 60, y: 28)
default: return nil
}
guard let view, !view.bounds.isEmpty else { return nil }
return view.convert(NSPoint(x: view.bounds.midX, y: view.bounds.midY), to: nil)
}
static func interceptDrags(in window: NSWindow) {
target = window
let method = class_getInstanceMethod(type(of: window), #selector(NSWindow.performDrag(with:)))!
let original = unsafeBitCast(method_getImplementation(method),
to: (@convention(c) (NSWindow, Selector, NSEvent) -> Void).self)
let handler: @convention(block) (NSWindow, NSEvent) -> Void = { window, event in
MainActor.assumeIsolated {
if window === target && dragDepth == 0 { drags += 1 }
dragDepth += 1
original(window, #selector(NSWindow.performDrag(with:)), event)
dragDepth -= 1
}
}
implementations.append((method, method_setImplementation(method, imp_implementationWithBlock(handler))))
// SwiftUI uses AppKit's completion-taking entry point on newer macOS releases.
let selector = NSSelectorFromString("performWindowDragWithEvent:completionHandler:")
if let method = class_getInstanceMethod(type(of: window), selector) {
let original = unsafeBitCast(method_getImplementation(method),
to: (@convention(c) (NSWindow, Selector, NSEvent, AnyObject?) -> Void).self)
let handler: @convention(block) (NSWindow, NSEvent, AnyObject?) -> Void = { window, event, done in
MainActor.assumeIsolated {
if window === target && dragDepth == 0 { drags += 1 }
dragDepth += 1
original(window, selector, event, done)
dragDepth -= 1
}
}
implementations.append((method, method_setImplementation(method, imp_implementationWithBlock(handler))))
}
}
static func advance() {
guard ContinuousClock.now < deadline else {
finish("Window drag timed out at \(names[probe]), phase \(phase)"); return
}
guard let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
let model = PlayerModel.shared
let fixtures = URL(fileURLWithPath: CommandLine.arguments.last!, isDirectory: true)
if target == nil {
guard model.canTogglePlayback else { return }
interceptDrags(in: window)
model.setVolume(0)
model.setPlayback(playing: false)
if CommandLine.arguments.contains("--queue-add-only") { model.showsQueue = true }
window.makeKeyAndOrderFront(nil)
window.makeMain()
NSApp.activate(ignoringOtherApps: true)
}
if phase == 0 {
guard NSApp.isActive, window.isKeyWindow, window.isMainWindow else {
window.makeKeyAndOrderFront(nil)
window.makeMain()
NSApp.activate(ignoringOtherApps: true)
return
}
model.setVolume(0)
switch probe {
case 5: model.closeFile()
case 8:
urlWindowFrame = window.frame
clickStartURL(in: window)
case 9:
model.open(fixtures.appendingPathComponent("cache.mp4"))
model.showsQueue = true
case 13:
model.showsQueue = false
model.openStream("not a video URL")
case 15: model.open(fixtures.appendingPathComponent("audio.wav"))
case 16: model.open(fixtures.appendingPathComponent("cache.mp4"))
case 19: model.hideControls(hideCursor: false)
default: break
}
previousDrags = drags
} else if phase == 1 {
guard !model.isQueueTransitioning, !model.compactWindow.isTransitioning, !model.isBusy else { return }
if probe == 16 && !model.isCompact { model.toggleCompact(); return }
// Posted events do not move the physical pointer into the compact window.
if probe != 19 { model.setPointerInsideWindow(true) }
guard let point = point(in: window) else { return }
start = point
} else if (2...4).contains(phase) {
let type: NSEvent.EventType = phase == 2 ? .leftMouseDown : (phase == 3 ? .leftMouseDragged : .leftMouseUp)
let point = phase == 2 ? start : NSPoint(x: start.x + 20, y: start.y + 48)
let event = NSEvent.mouseEvent(with: type, location: point, modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
context: nil, eventNumber: probe * 8 + phase, clickCount: 1, pressure: phase == 4 ? 0 : 1)!
let native = event.cgEvent!
if type == .leftMouseDragged {
native.setDoubleValueField(.mouseEventDeltaX, value: 20)
native.setDoubleValueField(.mouseEventDeltaY, value: -48)
}
NSApp.postEvent(NSEvent(cgEvent: native)!, atStart: false)
} else if phase == 6 {
let expected = controls.contains(probe) ? 0 : 1
guard drags - previousDrags == expected else {
finish("\(names[probe]): expected \(expected) native drag requests, got \(drags - previousDrags)"); return
}
if probe == 8 {
guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else {
finish("Revealing the URL field did not focus its editor"); return
}
editor.insertText(urlDraft, replacementRange: NSRange(location: NSNotFound, length: 0))
clickStartURL(in: window)
phase = 7
return
}
if probe == 12 {
urlWindowFrame = window.frame
clickQueueAdd(in: window, link: false)
phase = 7
return
}
probe += 1
if probe == names.count { finish(); return }
phase = 0
return
} else if probe == 8 && phase == 7 {
guard (window.firstResponder as? NSTextView)?.isFieldEditor != true else {
finish("Collapsing the URL field retained keyboard focus"); return
}
clickStartURL(in: window) // Reverse the collapse before its animation completes.
} else if probe == 8 && phase == 8 {
guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else { return }
guard editor.string == urlDraft else { finish("Reopening the URL field lost its draft"); return }
clickStartURL(in: window)
} else if probe == 8 && phase == 9 {
guard nativeView(in: window.contentView!, matching: { ($0 as? NSTextField)?.isEditable == true }) == nil else { return }
guard (window.firstResponder as? NSTextView)?.isFieldEditor != true,
window.frame == urlWindowFrame, drags == previousDrags else {
finish("URL-field transitions retained focus, resized or dragged the start window"); return
}
probe += 1
phase = 0
return
} else if probe == 12 && phase == 7 {
guard NSApp.modalWindow == nil else { finish("Queue + opened the file picker instead of expanding"); return }
clickQueueAdd(in: window, link: true)
} else if probe == 12 && phase == 8 {
guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else { return }
editor.insertText(urlDraft, replacementRange: NSRange(location: NSNotFound, length: 0))
clickQueueAdd(in: window, link: true)
} else if probe == 12 && phase == 9 {
guard nativeView(in: window.contentView!, matching: { ($0 as? NSTextField)?.isEditable == true }) == nil else { return }
guard (window.firstResponder as? NSTextView)?.isFieldEditor != true else {
finish("Collapsing the queue URL field retained keyboard focus"); return
}
clickQueueAdd(in: window, link: true)
} else if probe == 12 && phase == 10 {
guard let editor = window.firstResponder as? NSTextView, editor.isFieldEditor else { return }
guard editor.string == urlDraft else { finish("Reopening queue URL input lost its draft"); return }
let escape = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
context: nil, characters: "\u{1b}", charactersIgnoringModifiers: "\u{1b}", isARepeat: false, keyCode: 53)!
NSApp.postEvent(escape, atStart: false)
} else if probe == 12 && phase == 11 {
guard nativeView(in: window.contentView!, matching: { ($0 as? NSTextField)?.isEditable == true }) == nil else { return }
guard (window.firstResponder as? NSTextView)?.isFieldEditor != true,
window.frame == urlWindowFrame, drags == previousDrags else {
finish("Queue URL transitions retained focus, resized or dragged the window"); return
}
clickQueueAdd(in: window, link: false)
} else if probe == 12 && phase == 12 {
clickQueueAdd(in: window, link: false)
} else if probe == 12 && phase == 13 {
guard let panel = NSApp.modalWindow as? NSOpenPanel else { return }
guard panel.allowsMultipleSelection && panel.prompt == "Add to Queue" else {
panel.cancel(nil)
finish("Queue file icon did not open the multi-file queue picker"); return
}
panel.cancel(nil)
} else if probe == 12 && phase == 14 {
guard NSApp.modalWindow == nil else { return }
probe += 1
if CommandLine.arguments.contains("--queue-add-only") { finish(); return }
phase = 0
return
}
phase += 1
}
}
@MainActor
private enum QueueDragCheck {
static var timer: Timer?
static var step = 0
static var resultURL: URL!
static var original: [UUID] = []
static var current: UUID?
static var start = NSPoint.zero
static var end = NSPoint.zero
static var previousPoint = NSPoint.zero
// Deadline only; ordering assertions do not depend on the wall clock.
static let deadline = ContinuousClock.now.advanced(by: .seconds(20))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
_ = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) {
NSLog("[DEBUG-queue-drag] received %@", String(describing: $0))
return $0
}
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
// Keep injecting mouse events while SwiftUI tracks a pressed row.
RunLoop.main.add(timer!, forMode: .common)
terebiAppMain()
}
static func finish(_ error: String? = nil) {
timer?.invalidate()
try! JSONSerialization.data(withJSONObject: ["passed": error == nil, "error": error ?? ""])
.write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func mouse(_ type: NSEvent.EventType, at point: NSPoint, in window: NSWindow) {
let event = NSEvent.mouseEvent(with: type, location: point, modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil,
eventNumber: step, clickCount: 1, pressure: type == .leftMouseUp ? 0 : 1)!
let native = event.cgEvent!
if type == .leftMouseDragged {
native.setDoubleValueField(.mouseEventDeltaX, value: point.x - previousPoint.x)
native.setDoubleValueField(.mouseEventDeltaY, value: previousPoint.y - point.y)
}
previousPoint = point
NSApp.postEvent(NSEvent(cgEvent: native)!, atStart: false)
}
static func advance() {
let model = PlayerModel.shared
guard ContinuousClock.now < deadline else {
finish("Queue interaction did not complete at step \(step): status \(model.state.status), media \(model.fileName ?? "none"), windows \(NSApp.windows.count)")
return
}
guard model.canTogglePlayback,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }),
let content = window.contentView else { return }
if step == 0 {
model.setVolume(0)
let fixtures = model.queue.currentURL!.deletingLastPathComponent()
model.addToQueue(["av.mp4", "cache.mp4"].map { fixtures.appendingPathComponent($0) })
current = model.queue.currentID
original = model.queue.items.map(\.id)
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
} else if step == 1 {
guard !model.isQueueTransitioning else { return }
guard NSApp.isActive && window.isKeyWindow else {
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
return
}
window.layoutIfNeeded()
if let bitmap = content.bitmapImageRepForCachingDisplay(in: content.bounds) {
content.cacheDisplay(in: content.bounds, to: bitmap)
try! bitmap.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: "/tmp/terebi-queue-debug.png"))
}
let top = content.isFlipped ? content.bounds.minY : content.bounds.maxY
let sign: CGFloat = content.isFlipped ? 1 : -1
start = content.convert(NSPoint(x: content.bounds.maxX - 100, y: top + sign * 82), to: nil)
end = NSPoint(x: start.x, y: start.y - 120)
NSLog("[DEBUG-queue-drag] start %@ end %@ hit %@", String(describing: start), String(describing: end),
String(describing: content.hitTest(content.convert(start, from: nil))))
mouse(.leftMouseDown, at: start, in: window)
} else if step == 2 {
mouse(.leftMouseDragged, at: NSPoint(x: start.x, y: start.y - 10), in: window)
} else if step == 3 {
mouse(.leftMouseDragged, at: end, in: window)
} else if step == 12 {
guard model.queue.items.map(\.id) == original else {
finish("Queue committed before release"); return
}
// Several event-loop turns without a mouseDragged event, then release in place.
mouse(.leftMouseUp, at: end, in: window)
} else if step == 14 {
guard model.queue.items.map(\.id) == [original[1], original[2], original[0]],
model.queue.currentID == current else {
finish("Stationary release order: \(model.queue.items.map(\.url.lastPathComponent))"); return
}
mouse(.leftMouseDown, at: end, in: window)
} else if step == 15 {
mouse(.leftMouseDragged, at: NSPoint(x: end.x, y: end.y + 10), in: window)
} else if step == 16 {
mouse(.leftMouseDragged, at: start, in: window)
} else if step == 25 {
mouse(.leftMouseUp, at: start, in: window)
} else if step == 27 {
guard model.queue.items.map(\.id) == original, model.queue.currentID == current else {
finish("Stationary upward release did not restore the original order"); return
}
let secondRow = NSPoint(x: start.x, y: start.y - 60)
mouse(.leftMouseDown, at: secondRow, in: window)
} else if step == 28 {
mouse(.leftMouseUp, at: NSPoint(x: start.x, y: start.y - 60), in: window)
} else if step == 35 {
guard model.queue.currentID == original[1],
model.queue.items.map(\.id) == original,
model.state.status == PLAYER_PLAYING else {
finish("Single click did not play the second row without reordering"); return
}
finish()
return
}
step += 1
}
}
@MainActor
private enum WindowLayoutCheck {
static var timer: Timer?
static var resultURL: URL!
static var step = 0
static var windowIdentity: ObjectIdentifier?
static var normalFrame = NSRect.zero
static var animationStart: NSRect?
static var animationTarget = NSRect.zero
static var sawIntermediateFrame = false
static var animatedTransitions = 0
static var animatedQueueTransitions = 0
static var queueVideoSize: NSSize?
static var observingQueue = false
static var animationQueueWidth: CGFloat = 0
static var expectsAnimation: Bool { !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion }
// Infrastructure timeout only; assertions depend on native transition completion.
static let deadline = ContinuousClock.now.advanced(by: .seconds(30))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
_ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification,
object: nil, queue: .main) { _ in
MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) }
}
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification,
object: nil, queue: .main) { _ in
MainActor.assumeIsolated {
for window in NSApp.windows { window.alphaValue = 0; window.ignoresMouseEvents = true }
}
}
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
terebiAppMain()
}
static func finish(_ error: String? = nil) {
timer?.invalidate()
let result: [String: Any] = ["passed": error == nil, "error": error ?? "", "steps": step,
"animatedTransitions": animatedTransitions,
"animatedQueueTransitions": animatedQueueTransitions,
"reduceMotion": !expectsAnimation]
try! JSONSerialization.data(withJSONObject: result).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func sameFrame(_ lhs: NSRect, _ rhs: NSRect) -> Bool {
abs(lhs.minX - rhs.minX) < 1 && abs(lhs.minY - rhs.minY) < 1 &&
abs(lhs.width - rhs.width) < 1 && abs(lhs.height - rhs.height) < 1
}
static func observeAnimation(from window: NSWindow, to target: NSRect) {
animationStart = window.frame
animationTarget = target
sawIntermediateFrame = false
animationQueueWidth = PlayerModel.shared.queueWidth
}
static func sidebarHeaderIsVisible(in window: NSWindow) -> Bool {
guard let content = window.contentView else { return false }
let header = NSRect(x: content.bounds.maxX - 84,
y: content.isFlipped ? content.bounds.minY : content.bounds.maxY - 32,
width: 84, height: 32)
guard let bitmap = content.bitmapImageRepForCachingDisplay(in: header) else { return false }
content.cacheDisplay(in: header, to: bitmap)
let scale = CGFloat(bitmap.pixelsWide) / header.width
// Each control must paint a visible glyph, not merely exist in the view tree.
return (0..<3).allSatisfy { index in
var visiblePixels = 0
// Fullscreen disables pinning; its glyph uses the native dimmed tint.
let minimumBrightness = index == 0 && window.styleMask.contains(.fullScreen) ? 0.25 : 0.5
for x in Int(CGFloat(index * 28) * scale)..<Int(CGFloat(index * 28 + 24) * scale) {
for y in 0..<bitmap.pixelsHigh {
if let color = bitmap.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB),
color.alphaComponent > 0.5,
min(color.redComponent, color.greenComponent, color.blueComponent) > minimumBrightness {
visiblePixels += 1
}
}
}
return visiblePixels >= 8
}
}
static func advance() {
let model = PlayerModel.shared
guard ContinuousClock.now < deadline else { finish("Window layout did not settle at step \(step)"); return }
guard model.canTogglePlayback, model.state.has_video,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
if model.compactWindow.isTransitioning || model.isQueueTransitioning {
window.layoutIfNeeded()
if model.isQueueTransitioning && !sidebarHeaderIsVisible(in: window) {
finish("Top-right controls left the window corner during queue animation at step \(step)"); return
}
if let size = queueVideoSize, let video = model.videoView {
guard abs(video.bounds.width - size.width) < 2, abs(video.bounds.height - size.height) < 2 else {
finish("Queue animation changed video size at step \(step): \(video.bounds.size), expected \(size)"); return
}
}
if expectsAnimation, let start = animationStart, !sawIntermediateFrame,
(!sameFrame(window.frame, start) && !sameFrame(window.frame, animationTarget) ||
observingQueue && abs(model.queueWidth - animationQueueWidth) > 1 &&
model.queueWidth > 1 && model.queueWidth < 259) {
sawIntermediateFrame = true
// These run-loop-delivered requests must remain responsive mid-animation.
if model.isQueueTransitioning {
let frame = window.frame
model.showsQueue.toggle()
model.showsQueue.toggle()
guard sameFrame(window.frame, frame) else { finish("Queue reversal jumped the window"); return }
} else {
model.toggleCompact()
model.toggleCompact()
}
}
return
}
if animationStart != nil {
if expectsAnimation {
guard sawIntermediateFrame else { finish("No nonblocking animation frames at step \(step)"); return }
if observingQueue { animatedQueueTransitions += 1 }
else { animatedTransitions += 1 }
}
animationStart = nil
guard sameFrame(window.frame, animationTarget) else {
finish("Animation missed target at step \(step): \(window.frame), expected \(animationTarget)"); return
}
queueVideoSize = nil
observingQueue = false
}
if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) }
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
if model.fullScreenIntent != nil { return }
window.layoutIfNeeded()
if model.showsQueue && !model.isCompact && !sidebarHeaderIsVisible(in: window) {
finish("Sidebar pin, Cast or settings glyph is not visible at step \(step)"); return
}
if step >= 33 {
switch step {
case 33, 34:
guard model.isFullScreen else { finish("Fullscreen entry failed"); return }
normalFrame = window.frame
observeAnimation(from: window, to: normalFrame)
observingQueue = true
model.showsQueue.toggle()
case 35:
guard sameFrame(window.frame, normalFrame), model.showsQueue,
let video = model.videoView, let content = window.contentView,
abs(video.bounds.width - (content.bounds.width - 260)) < 2 else {
finish("Fullscreen queue did not retain fixed window bounds and fit the video"); return
}
model.toggleFullScreen()
default:
guard !model.isFullScreen else { finish("Fullscreen exit failed"); return }
finish(); return
}
step += 1
return
}
switch step % 8 {
case 0, 4:
if step > 0 {
let actual = window.frame
guard abs(actual.minX - normalFrame.minX) < 1, abs(actual.minY - normalFrame.minY) < 1,
abs(actual.width - normalFrame.width) < 1, abs(actual.height - normalFrame.height) < 1,
!model.isCompact else { finish("Normal geometry was not restored at step \(step): \(actual)"); return }
}
if step == 32 {
model.toggleFullScreen()
step += 1
return
}
let showsQueue = step % 8 == 4
if showsQueue != model.showsQueue {
if showsQueue, let screen = window.screen?.visibleFrame {
window.setFrameOrigin(NSPoint(x: screen.maxX - window.frame.width, y: window.frame.minY))
}
var target = window.frame
target.size.width += showsQueue ? 260 : -260
if let screen = window.screen?.visibleFrame {
target.origin.x = max(screen.minX, min(target.minX, screen.maxX - target.width))
}
observeAnimation(from: window, to: target)
observingQueue = true
queueVideoSize = model.videoView?.bounds.size
model.showsQueue = showsQueue
}
case 1, 5:
window.setFrame(NSRect(x: 100, y: 200, width: 763, height: 368), display: true)
normalFrame = window.frame
guard window.minSize.width >= (model.showsQueue ? 660 : 400), window.minSize.height >= 240 else {
finish("Normal minimum size was lost: \(window.minSize)"); return
}
case 2, 6:
// Repeated intent changes must keep the original restoration frame.
observeAnimation(from: window, to: CompactWindowController.initialFrame(
aspect: model.state.video_aspect_ratio,
visibleFrame: (window.screen ?? NSScreen.main!).visibleFrame, width: 320))
model.toggleCompact()
model.toggleCompact()
model.toggleCompact()
default:
guard model.isCompact, window.frame.width >= 160, window.frame.height >= 90,
abs(window.frame.width / window.frame.height - model.state.video_aspect_ratio) < 0.01 else {
finish("Compact geometry did not settle at step \(step): \(window.frame)"); return
}
guard window.minSize.width >= 160, window.minSize.height >= 90 else {
finish("Compact minimum size was lost: \(window.minSize)"); return
}
observeAnimation(from: window, to: normalFrame)
model.toggleCompact()
}
step += 1
}
}
@MainActor
private enum AppStartupCheck {
static var model: PlayerModel?
static var timer: Timer?
static let deadline = ContinuousClock.now.advanced(by: .seconds(15))
static let historyDomain = Bundle.main.bundleIdentifier! + ".history"
static func run() {
_ = NSApplication.shared
NSApp.setActivationPolicy(.regular)
let defaults = UserDefaults(suiteName: historyDomain)!
model = PlayerModel(defaults: defaults)
model!.setVolume(0)
model!.open(URL(fileURLWithPath: CommandLine.arguments[2]).appendingPathComponent("av.mp4"))
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { finishWhenReady() }
}
NSApp.run()
}
static func finishWhenReady() {
guard let model else { return }
let ready = model.canTogglePlayback && model.state.status == PLAYER_PLAYING
guard ready || model.state.status == PLAYER_ERROR || ContinuousClock.now >= deadline else { return }
timer?.invalidate()
let pid = ProcessInfo.processInfo.processIdentifier
let application = NSRunningApplication.current
let recognized = application.processIdentifier == pid &&
NSWorkspace.shared.runningApplications.contains { $0.processIdentifier == pid && $0.bundleURL == Bundle.main.bundleURL }
let result: [String: Any] = [
"passed": ready && recognized && MPNowPlayingInfoCenter.default().playbackState == .playing,
"pid": pid,
"registeredPID": application.processIdentifier,
"bundleIdentifier": application.bundleIdentifier ?? "nil",
"playing": ready,
]
do {
let pipe = try FileHandle(forWritingTo: URL(fileURLWithPath: CommandLine.arguments[1]))
try pipe.write(contentsOf: JSONSerialization.data(withJSONObject: result))
try pipe.write(contentsOf: Data([10]))
try pipe.close()
} catch {
FileHandle.standardError.write(Data("Startup check could not report: \(error)\n".utf8))
}
model.shutdown()
UserDefaults(suiteName: historyDomain)!.removePersistentDomain(forName: historyDomain)
self.model = nil
NSApp.terminate(nil)
}
}
@MainActor
private enum CLIInstanceCheck {
static var timer: Timer?
static var requestID = ""
static var editorText = ""
static func selectMenuItem(_ title: String, in menu: NSMenu) -> Bool {
for (index, item) in menu.items.enumerated() {
if item.title == title, item.action != nil {
precondition(item.isEnabled, "Native menu action \(title) must be available")
menu.performActionForItem(at: index)
return true
}
if let submenu = item.submenu, selectMenuItem(title, in: submenu) { return true }
}
return false
}
static func prepareMenu(_ menu: NSMenu) {
// Direct key-equivalent dispatch does not open/track the native menu first.
// Ask AppKit to prepare and validate its real items just as normal menu use does.
menu.delegate?.menuNeedsUpdate?(menu)
menu.update()
for item in menu.items {
if let submenu = item.submenu { prepareMenu(submenu) }
}
}
static func captionSelector(in view: NSView) -> NSPopUpButton? {
if let selector = view as? NSPopUpButton, selector.toolTip == "Subtitles" { return selector }
for child in view.subviews {
if let selector = captionSelector(in: child) { return selector }
}
return nil
}
static func windowSnapshot(_ window: NSWindow?) -> [String: Any] {
guard let window, let content = window.contentView else { return [:] }
func surface(in view: NSView) -> NSView? {
if view.identifier?.rawValue == "PlaybackSurface" { return view }
return view.subviews.lazy.compactMap { surface(in: $0) }.first
}
var result: [String: Any] = [
"width": window.frame.width, "height": window.frame.height,
"minWidth": window.minSize.width, "minHeight": window.minSize.height,
"maxWidth": window.maxSize.width, "maxHeight": window.maxSize.height,
"contentMinWidth": window.contentMinSize.width, "contentMinHeight": window.contentMinSize.height,
"contentMaxWidth": window.contentMaxSize.width, "contentMaxHeight": window.contentMaxSize.height,
"aspectWidth": window.aspectRatio.width, "aspectHeight": window.aspectRatio.height,
"resizeIncrementWidth": window.resizeIncrements.width,
"resizeIncrementHeight": window.resizeIncrements.height,
"resizable": window.styleMask.contains(.resizable),
]
if let surface = surface(in: content) {
func hit(_ point: NSPoint) -> NSView? {
content.hitTest(surface.convert(point, to: content.superview))
}
result["videoOwnsMouse"] = hit(NSPoint(x: surface.bounds.midX, y: surface.bounds.midY)) === surface
result["controlsOwnMouse"] = hit(NSPoint(x: surface.bounds.midX, y: 46)) !== surface
result["sidebarOwnsMouse"] = hit(NSPoint(x: surface.bounds.maxX + 130, y: surface.bounds.midY)) !== surface
result["videoWidth"] = surface.bounds.width
result["videoHeight"] = surface.bounds.height
if let video = PlayerModel.shared.videoView {
result["rendererWidth"] = video.bounds.width
result["rendererHeight"] = video.bounds.height
result["rendererAttached"] = video.superview === surface
result["rendererHidden"] = video.isHidden
}
result["surfaceMovesOnMouseDown"] = surface.mouseDownCanMoveWindow
let edges = [NSPoint(x: 1, y: content.bounds.midY),
NSPoint(x: content.bounds.maxX - 1, y: content.bounds.midY),
NSPoint(x: content.bounds.midX, y: 1),
NSPoint(x: content.bounds.midX, y: content.bounds.maxY - 1)]
let targets = edges.map { point in
content.hitTest(content.convert(point, to: content.superview))
}
result["edgeTargets"] = targets.map { $0.map { String(describing: type(of: $0)) } ?? "native window" }
result["edgesAvoidDragSurface"] = targets.allSatisfy { target in
target !== surface && !(target.map { String(describing: type(of: $0)).contains("ChromeView") } ?? false)
}
}
return result
}
static func run(_ configuration: URL) {
let settings = try! JSONSerialization.jsonObject(with: Data(contentsOf: configuration)) as! [String: String]
let directory = URL(fileURLWithPath: settings["directory"]!, isDirectory: true)
let stderrURL = directory.appendingPathComponent("stderr-\(ProcessInfo.processInfo.processIdentifier).log")
precondition(FileManager.default.createFile(atPath: stderrURL.path, contents: nil))
let errorLog = try! FileHandle(forWritingTo: stderrURL)
precondition(dup2(errorLog.fileDescriptor, STDERR_FILENO) >= 0)
try! errorLog.close()
setenv("PATH", settings["path"]!, 1)
_ = NSApplication.shared
_ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification,
object: nil, queue: .main) { _ in
MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) }
}
// Exercise the real SwiftUI scene and delegate without displaying test windows.
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification, object: nil, queue: .main) { _ in
MainActor.assumeIsolated {
for window in NSApp.windows {
window.alphaValue = 0
window.ignoresMouseEvents = true
}
}
}
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated {
let model = PlayerModel.shared
let pid = ProcessInfo.processInfo.processIdentifier
// Text input can create non-main auxiliary windows; count actual player windows.
let windows = NSApp.windows.filter { $0.canBecomeMain && !($0 is NSPanel) && $0.contentView != nil }
for window in windows { window.alphaValue = 0 }
let request = directory.appendingPathComponent("keys-\(pid).json")
var pendingRequestID = ""
if let data = try? Data(contentsOf: request),
let command = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let id = command["id"] as? String, id != requestID, let window = windows.first {
pendingRequestID = id
if !NSApp.isActive || !window.isKeyWindow || !window.isMainWindow {
// Synthetic native key requests need foreground focus; cooperative activation
// can be refused by the unrelated app currently active on the workstation.
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
window.makeMain()
} else {
requestID = id
if let media = command["media"] as? String { model.open(URL(fileURLWithPath: media)) }
if let action = command["action"] as? String {
switch action {
case "youtube":
model.openStream(command["url"] as! String)
case "queue":
model.showsQueue = command["visible"] as! Bool
model.revealControls()
case "queueAdvance":
model.advanceQueue(command["offset"] as! Int)
case "caption":
guard let view = window.contentView, let selector = captionSelector(in: view),
let cell = selector.cell as? NSPopUpButtonCell, let menu = selector.menu else {
fatalError("Native captions selector is missing")
}
cell.attachPopUp(withFrame: selector.bounds, in: selector)
menu.update()
let label = command["label"] as! String
guard let item = menu.items.first(where: { $0.title == label }) else {
fatalError("Requested native subtitle selection is missing")
}
precondition(!item.isHidden && item.isEnabled, "Subtitle selection must be visible and enabled")
cell.dismissPopUp()
precondition(selectMenuItem(label, in: menu), "Native subtitle selection must be actionable")
case "seek":
let position = command["position"] as! Double
precondition(model.seekRelative(position - model.state.position), "Native seek must be accepted")
case "pause":
precondition(model.perform(.pause), "Native pause must be accepted")
case "play":
precondition(model.perform(.play), "Native play must be accepted")
case "quality":
precondition(model.canChangeYouTubeQuality, "Native quality change must be available")
model.setYouTubeQuality((command["height"] as! Int))
case "close":
model.closeFile()
case "quit":
timer?.invalidate()
precondition(model.perform(.quit), "Native quit must be accepted")
return
default:
fatalError("Unknown native check action \(action)")
}
}
if let events = command["events"] as? [[String: Any]] {
window.makeKey()
for input in events {
if let title = input["select"] as? String {
guard let menu = NSApp.mainMenu else { fatalError("Native application menu is missing") }
prepareMenu(menu)
precondition(selectMenuItem(title, in: menu), "Native menu action \(title) must exist")
continue
}
let editor = NSTextView(frame: .zero)
if input["editing"] as? Bool == true {
window.contentView?.addSubview(editor)
window.makeFirstResponder(editor)
} else { window.makeFirstResponder(nil) }
let text = input["key"] as! String
let event = NSEvent.keyEvent(with: .keyDown, location: .zero,
modifierFlags: NSEvent.ModifierFlags(rawValue: UInt(input["flags"] as? Int ?? 0)),
timestamp: 0, windowNumber: window.windowNumber, context: nil,
characters: text, charactersIgnoringModifiers: text,
isARepeat: input["repeat"] as? Bool ?? false,
keyCode: UInt16(input["code"] as? Int ?? 7))!
if input["menu"] as? Bool == true {
if let menu = NSApp.mainMenu { prepareMenu(menu) }
precondition(NSApp.mainMenu?.performKeyEquivalent(with: event) == true,
"Configured shortcut must be accepted by the native menu")
} else if input["queued"] as? Bool == true {
NSApp.postEvent(event, atStart: false)
} else { NSApp.sendEvent(event) }
if input["editing"] as? Bool == true && input["queued"] as? Bool != true {
editorText = editor.string
window.makeFirstResponder(nil)
editor.removeFromSuperview()
}
}
}
}
}
let applications = NSWorkspace.shared.runningApplications.filter {
!$0.isTerminated && $0.bundleURL == Bundle.main.bundleURL
}
let snapshot: [String: Any] = [
"pid": pid,
"pids": applications.map(\.processIdentifier).sorted(),
"media": model.fileName ?? "",
"status": model.state.status.rawValue,
"position": model.state.position,
"duration": model.state.duration,
"seekPending": model.state.seek_pending,
"canSeek": model.canSeek,
"busy": model.isBusy,
"youtubeHeight": model.youtubeHeight ?? 0,
"canChangeQuality": model.canChangeYouTubeQuality,
"qualityHeights": model.youtubeHeights,
"windows": windows.count,
"hasAudio": model.state.has_audio,
"hasVideo": model.state.has_video,
"error": model.errorText ?? "",
"subtitleLabels": model.subtitleTracks.map(\.label),
"selectedSubtitle": model.selectedSubtitle ?? -1,
"subtitleText": model.subtitleText,
"subtitleError": model.subtitleError ?? "",
"showBufferInfo": model.showsBufferInfo,
"automaticQuality": model.automaticYouTubeQuality,
"snapToEdges": model.compactWindow.snappingEnabled,
"requestID": requestID,
"pendingRequestID": pendingRequestID,
"active": NSApp.isActive,
"keyWindow": windows.first?.isKeyWindow ?? false,
"mainWindow": windows.first?.isMainWindow ?? false,
"editorText": editorText,
"compact": model.isCompact,
"showsQueue": model.showsQueue,
"queueURLs": model.queue.items.map(\.url.absoluteString),
"queueIndex": model.queue.items.firstIndex { $0.id == model.queue.currentID } ?? -1,
"window": windowSnapshot(windows.first),
"onTop": model.isOnTop,
]
do {
let data = try JSONSerialization.data(withJSONObject: snapshot)
try data.write(to: directory.appendingPathComponent("state-\(pid).json"), options: .atomic)
} catch { fatalError("Could not publish CLI instance check: \(error)") }
}
}
terebiAppMain()
}
}