import Foundation import TOMLDecoder public struct PlayerConfiguration: Equatable, Sendable { public var showBufferInfo: Bool public var automaticQuality: Bool public var snapToEdges: Bool public init(showBufferInfo: Bool = false, automaticQuality: Bool = false, snapToEdges: Bool = true) { self.showBufferInfo = showBufferInfo self.automaticQuality = automaticQuality self.snapToEdges = snapToEdges } public static func fileURL(environment: [String: String], homeDirectory: URL) -> URL { let directory: URL if let path = environment["XDG_CONFIG_HOME"], path.hasPrefix("/") { directory = URL(fileURLWithPath: path, isDirectory: true) } else { directory = homeDirectory.appendingPathComponent(".config", isDirectory: true) } return directory.appendingPathComponent("terebi/config.toml") } public static func loadForApplication(arguments: [String], environment: [String: String], homeDirectory: URL) throws -> Self { if arguments.contains("--no-config") { return Self() } if let index = arguments.firstIndex(of: "--config-file") { guard index + 1 < arguments.count, let url = URL(string: arguments[index + 1]), url.isFileURL, url.host == nil || url.host == "" || url.host == "localhost" else { throw ConfigurationError(message: "--config-file requires a local file URL.") } return try load(from: url) } return try load(from: fileURL(environment: environment, homeDirectory: homeDirectory)) } public static func load(from url: URL) throws -> Self { let data: Data do { data = try Data(contentsOf: url) } catch { let failure = error as NSError let underlying = failure.userInfo[NSUnderlyingErrorKey] as? NSError if failure.domain == NSCocoaErrorDomain, failure.code == CocoaError.fileReadNoSuchFile.rawValue, underlying?.domain == NSPOSIXErrorDomain, underlying?.code == Int(ENOENT) { return Self() } throw ConfigurationError(message: "\(url.path): \(error.localizedDescription)") } do { let values = try TOMLDecoder(isLenient: false).decode([String: Bool].self, from: data) for key in values.keys { switch key { case "show_buffer_info", "automatic_quality", "snap_to_edges": break default: throw ConfigurationError(message: "Unknown setting '\(key)'.") } } var configuration = Self() configuration.showBufferInfo = values["show_buffer_info"] ?? configuration.showBufferInfo configuration.automaticQuality = values["automatic_quality"] ?? configuration.automaticQuality configuration.snapToEdges = values["snap_to_edges"] ?? configuration.snapToEdges return configuration } catch { throw ConfigurationError(message: "\(url.path): \(diagnostic(for: error))") } } private static func diagnostic(for error: Error) -> String { let context: DecodingError.Context switch error { case DecodingError.dataCorrupted(let value), DecodingError.typeMismatch(_, let value), DecodingError.valueNotFound(_, let value), DecodingError.keyNotFound(_, let value): context = value case let error as ConfigurationError: return error.message default: return String(describing: error) } if let underlying = context.underlyingError { return String(describing: underlying) } let path = context.codingPath.map(\.stringValue).joined(separator: ".") return path.isEmpty ? context.debugDescription : "\(path): \(context.debugDescription)" } private struct ConfigurationError: LocalizedError { let message: String var errorDescription: String? { message } } }