Files
terebi/Sources/Configuration/PlayerConfiguration.swift
T

87 lines
4.0 KiB
Swift

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 }
}
}