import AppKit import Darwin import MediaPlayer @_cdecl("terebi_app_main") @MainActor public func checkAppStartup() { 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 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 _ = 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() } } 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 drag did not complete at step \(step)"); 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) model.addToQueue(["B", "C"].map { URL(fileURLWithPath: "/queue-drag-check/\($0).mp4") }) 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 } 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).. 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.openYouTube(command["url"] as! String) case "queue": model.showsQueue = command["visible"] as! Bool model.revealControls() 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, "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() } }