feat: load optional XDG TOML configuration with session overrides
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user