diff --git a/Checks/configuration.swift b/Checks/configuration.swift index fef5aaf..f6baec1 100644 --- a/Checks/configuration.swift +++ b/Checks/configuration.swift @@ -110,6 +110,10 @@ struct ConfigurationChecks { let fallback = home.appendingPathComponent(".config/terebi/config.toml") let config = xdg.appendingPathComponent("terebi/config.toml") let defaults = PlayerConfiguration() + let printed = root.appendingPathComponent("printed-defaults.toml") + try Data(PlayerConfiguration.defaultTOML.utf8).write(to: printed) + try require(try PlayerConfiguration.load(from: printed) == defaults, + "printed TOML is directly usable and preserves all default behavior and bindings") try require(!defaults.showBufferInfo && !defaults.automaticQuality && defaults.snapToEdges, "built-in defaults hide buffer info, disable automatic quality, and enable snapping") try require(PlayerConfiguration.fileURL(environment: ["XDG_CONFIG_HOME": xdg.path], homeDirectory: home) == config, diff --git a/Checks/player-command.swift b/Checks/player-command.swift index ba25925..603a430 100644 --- a/Checks/player-command.swift +++ b/Checks/player-command.swift @@ -55,6 +55,9 @@ struct PlayerCommandChecks { guard case .help = try PlayerCommand.parse(["--new-instance", "--help"]) else { fatalError("FAIL: help must not launch an independent player") } + guard case .printDefaultConfig = try PlayerCommand.parse(["--print-default-config"]) else { + fatalError("FAIL: printing defaults must not launch a player") + } for arguments in [["--url"], ["--url", canonical.absoluteString, movie.path], ["--"], [movie.path, movie.path], ["--unknown"], ["missing.mp4"], [directory.path], @@ -62,6 +65,7 @@ struct PlayerCommandChecks { ["--new-instance", "--new-instance"], ["--new-instance", "--url"], ["--new-instance", "--"], ["--new-instance", movie.path, movie.path], ["--new-instance", "--unknown"], ["--no-config", "--no-config"], + ["--print-default-config", movie.path], ["--print-default-config", "--print-default-config"], ["--no-config", "--new-instance", "--no-config"]] { do { _ = try PlayerCommand.parse(arguments, directory: directory) diff --git a/README.md b/README.md index 2528337..351047e 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,14 @@ The declared app executable stays running after preflight and loads `Contents/Fr .build/terebi --new-instance --url 'https://www.youtube.com/watch?v=jNQXAC9IVRw' .build/terebi --new-instance --no-config "/path/to/video.mp4" .build/terebi --help +.build/terebi --print-default-config ``` One local file (including a local `file://` URL) or YouTube video URL is accepted. Paths are relative to the shell's current directory; quote spaces and URL query strings. `--` treats the following argument as a filename. With no arguments, the CLI opens or activates the player without replacing its media. Put **`--new-instance` before the media arguments** to launch a separate player process/window instead: it works without media, with a local file or positional YouTube URL, and with `--url` or `--`. Existing playback stays untouched. To use `terebi` by name, add the build directory to your shell's PATH: `export PATH="$PWD/.build:$PATH"`. The launcher finds `terebi.app` beside its own executable, following symlinks. By default, macOS application/URL delivery reuses a running player and its existing window. With `--new-instance`, one fresh application launch carries any media URL as a startup argument, so it reaches that process without a second app launch or changing another player's media. Keep the executable and app bundle together; this is not a standalone distribution. Media opens use the same autoplay and saved-progress behavior as the UI. Instances share saved progress, but configuration and control overrides belong to each session. Quit all running players after installing this update so every process uses the updated configuration/startup handling. -Help exits with status **0** without opening the GUI, reading configuration, or loading FFmpeg. Invalid arguments, unsupported URLs, missing media files, and directories report to stderr and exit **64**; unavailable FFmpeg libraries and application-launch failures exit **69**; unreadable or invalid configuration exits **78** with the file path and diagnostic. A successful launch exits **0** immediately after macOS accepts the request, not when playback finishes. Later extraction/decoding failures appear in the player window; this is a GUI launcher, not a headless player or remote playback-control interface. +Help and `--print-default-config` exit with status **0** without opening the GUI, reading configuration, or checking/loading FFmpeg. The latter prints complete built-in TOML to stdout, including every named key action and explicit `[]` entries for unbound actions; it uses the player's authoritative defaults rather than your current settings. It accepts no media argument and never creates or writes a config file. Invalid arguments, unsupported URLs, missing media files, and directories report to stderr and exit **64**; unavailable FFmpeg libraries and application-launch failures exit **69**; unreadable or invalid configuration exits **78** with the file path and diagnostic. A successful launch exits **0** immediately after macOS accepts the request, not when playback finishes. Later extraction/decoding failures appear in the player window; this is a GUI launcher, not a headless player or remote playback-control interface. ## Configuration diff --git a/Sources/CLI/PlayerCommand.swift b/Sources/CLI/PlayerCommand.swift index fd48034..e43a28e 100644 --- a/Sources/CLI/PlayerCommand.swift +++ b/Sources/CLI/PlayerCommand.swift @@ -2,6 +2,7 @@ import Foundation enum PlayerCommand { case help + case printDefaultConfig case launch(URL?, newInstance: Bool, noConfig: Bool) static let usage = """ @@ -9,6 +10,7 @@ enum PlayerCommand { terebi [--new-instance] [--no-config] --url YOUTUBE_URL terebi [--new-instance] [--no-config] -- FILE terebi --help + terebi --print-default-config Open one local media file or YouTube video and start playback, resuming saved video progress when available. With no arguments, open or activate the player. @@ -19,6 +21,7 @@ enum PlayerCommand { --url URL Open a YouTube video URL. -- Treat the following argument as a filename, even with a leading '-'. -h, --help Show this help without launching the player. + --print-default-config Print the complete built-in TOML without launching. Reuses the running player unless --new-instance is supplied before the media arguments. The command exits once macOS accepts the launch; playback and @@ -41,6 +44,7 @@ enum PlayerCommand { } if inputs.isEmpty { return .launch(nil, newInstance: newInstance, noConfig: noConfig) } if inputs == ["--help"] || inputs == ["-h"] { return .help } + if inputs == ["--print-default-config"] { return .printDefaultConfig } if inputs.first == "--url" { guard inputs.count == 2 else { throw ArgumentError("--url requires exactly one YouTube URL.") } return .launch(try YouTubeResolver.validateURL(inputs[1]), newInstance: newInstance, noConfig: noConfig) diff --git a/Sources/CLI/TerebiCLI.swift b/Sources/CLI/TerebiCLI.swift index a161a27..95f6ec2 100644 --- a/Sources/CLI/TerebiCLI.swift +++ b/Sources/CLI/TerebiCLI.swift @@ -15,6 +15,8 @@ enum TerebiCLI { switch command { case .help: print(PlayerCommand.usage) + case .printDefaultConfig: + print(PlayerConfiguration.defaultTOML, terminator: "") case .launch(let url, let newInstance, let noConfig): let configURL = PlayerConfiguration.fileURL(environment: ProcessInfo.processInfo.environment, homeDirectory: FileManager.default.homeDirectoryForCurrentUser) diff --git a/Sources/Configuration/PlayerConfiguration.swift b/Sources/Configuration/PlayerConfiguration.swift index 028f211..a3a4a1c 100644 --- a/Sources/Configuration/PlayerConfiguration.swift +++ b/Sources/Configuration/PlayerConfiguration.swift @@ -15,6 +15,26 @@ public struct PlayerConfiguration: Equatable, Sendable { self.keys = keys } + public static var defaultTOML: String { + let defaults = Self() + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + var lines = [ + "automatic_quality = \(defaults.automaticQuality)", + "", "[ui]", + "buffer_info = \(defaults.showBufferInfo)", + "compact_snap_to_edges = \(defaults.snapToEdges)", + "", "[keys]" + ] + for (name, action) in PlayerAction.namedActions { + let chords = defaults.keys.shortcuts(for: action).map(\.description) + // JSON string arrays are valid TOML arrays; preserve escaping for printable keys. + let value = String(decoding: try! encoder.encode(chords), as: UTF8.self) + lines.append("\(name) = \(value)") + } + return lines.joined(separator: "\n") + "\n" + } + public static func fileURL(environment: [String: String], homeDirectory: URL) -> URL { let directory: URL if let path = environment["XDG_CONFIG_HOME"], path.hasPrefix("/") {