import AppKit import Darwin import TerebiConfiguration @main @MainActor enum TerebiCLI { static func main() async { let command: PlayerCommand do { command = try PlayerCommand.parse(Array(CommandLine.arguments.dropFirst())) } catch { fail(error.localizedDescription, code: 64) } switch command { case .help: print(PlayerCommand.usage) case .printDefaultConfig: print(PlayerConfiguration.defaultTOML, terminator: "") case .launch(let url, let newInstance, let noConfig, let progressFile): await launch(url: url, sourceFile: nil, newInstance: newInstance, noConfig: noConfig, progressFile: progressFile) case .launchSource(let sourceFile, let newInstance, let noConfig, let progressFile): await launch(url: nil, sourceFile: sourceFile, newInstance: newInstance, noConfig: noConfig, progressFile: progressFile) case .launchQueue(let queueFile, let newInstance, let noConfig): await launch(url: nil, sourceFile: nil, newInstance: newInstance, noConfig: noConfig, progressFile: nil, queueFile: queueFile) } } private static func launch(url: URL?, sourceFile: URL?, newInstance: Bool, noConfig: Bool, progressFile: URL?, queueFile: URL? = nil) async { if let progressFile { var existing = stat() guard lstat(progressFile.path, &existing) != 0 else { fail("Progress file already exists: \(progressFile.path). Supply a nonexistent destination.", code: 73) } guard errno == ENOENT else { fail("Could not inspect progress file \(progressFile.path): \(String(cString: strerror(errno))).", code: 73) } } let configURL = PlayerConfiguration.fileURL(environment: ProcessInfo.processInfo.environment, homeDirectory: FileManager.default.homeDirectoryForCurrentUser) if !noConfig { do { _ = try PlayerConfiguration.load(from: configURL) } catch { fail(error.localizedDescription, code: 78) } } var size: UInt32 = 0 _ = _NSGetExecutablePath(nil, &size) var path = [CChar](repeating: 0, count: Int(size)) guard _NSGetExecutablePath(&path, &size) == 0 else { fail("Could not locate the CLI executable.", code: 69) } let executable = path.withUnsafeBufferPointer { URL(fileURLWithPath: String(cString: $0.baseAddress!)).resolvingSymlinksInPath() } let macOS = executable.deletingLastPathComponent() let contents = macOS.deletingLastPathComponent() let app = contents.deletingLastPathComponent() guard macOS.lastPathComponent == "MacOS", contents.lastPathComponent == "Contents", app.pathExtension == "app" else { fail("The CLI must be installed inside terebi.app. Rebuild and reinstall the bundled app.", code: 69) } if let error = FFmpegRuntime.error(for: app) { fail(error, code: 69) } let configuration = NSWorkspace.OpenConfiguration() configuration.createsNewApplicationInstance = newInstance // LaunchServices does not inherit the invoking shell's XDG environment. configuration.arguments = noConfig ? ["--no-config"] : ["--config-file", configURL.absoluteString] if let progressFile { configuration.arguments += ["--progress-file", progressFile.path] } if let queueFile { configuration.arguments += ["--queue-file", queueFile.path] } if let sourceFile { configuration.arguments += ["--source-file", sourceFile.path] } if newInstance, let url { configuration.arguments += ["--open-media", url.absoluteString] } do { // Launch an application, not a document, so SwiftUI creates its singleton window. _ = try await NSWorkspace.shared.openApplication(at: app, configuration: configuration) if !newInstance, let url { _ = try await NSWorkspace.shared.open([url], withApplicationAt: app, configuration: configuration) } } catch { fail("Could not launch the player: \(error.localizedDescription)", code: 69) } } private static func fail(_ message: String, code: Int32) -> Never { FileHandle.standardError.write(Data("terebi: \(message)\n".utf8)) exit(code) } }