import Commander
import Foundation
// MARK: - Binder
enum CommanderCLIBinder {
static func instantiateCommand(
type: any ParsableCommand.Type,
parsedValues: ParsedValues
) throws -> any ParsableCommand {
var command = type.init()
let runtimeOptions = try self.makeRuntimeOptions(from: parsedValues)
if var bindable = command as? any CommanderBindableCommand {
try bindable.applyCommanderValues(.init(parsedValues: parsedValues))
guard let rebound = bindable as? any ParsableCommand else {
preconditionFailure("CommanderBindableCommand cast should always round-trip to original type \(type)")
}
command = rebound
}
if var configurable = command as? any RuntimeOptionsConfigurable {
configurable.setRuntimeOptions(runtimeOptions)
guard let rebound = configurable as? any ParsableCommand else {
preconditionFailure("RuntimeOptionsConfigurable cast should always round-trip to original type \(type)")
}
command = rebound
}
return command
}
static func instantiateCommand<T>(
ofType type: T.Type,
parsedValues: ParsedValues
) throws -> T where T: ParsableCommand {
guard let command = try instantiateCommand(type: type, parsedValues: parsedValues) as? T else {
preconditionFailure("Commander instantiation failed to produce expected type \(T.self)")
}
return command
}
static func makeRuntimeOptions(from parsedValues: ParsedValues) throws -> CommandRuntimeOptions {
var options = CommandRuntimeOptions()
options.verbose = parsedValues.flags.contains("verbose")
options.jsonOutput = parsedValues.flags.contains("jsonOutput")
let values = CommanderBindableValues(parsedValues: parsedValues)
if let level: LogLevel = try values.decodeOption("logLevel", as: LogLevel.self) {
options.logLevel = level
}
if let captureEngine = values.singleOption("captureEngine")?
.trimmingCharacters(in: .whitespacesAndNewlines),
!captureEngine.isEmpty {
options.captureEnginePreference = captureEngine
}
if values.flag("no-remote") {
options.preferRemote = false
}
if let xpc = values.singleOption("xpc-service")?.trimmingCharacters(in: .whitespacesAndNewlines), !xpc.isEmpty {
options.xpcServiceName = xpc
}
return options
}
}
// MARK: - Bindable Protocol
struct CommanderBindableValues {
let positional: [String]
let options: [String: [String]]
let flags: Set<String>
init(positional: [String], options: [String: [String]], flags: Set<String>) {
self.positional = positional
self.options = options
self.flags = flags
}
init(parsedValues: ParsedValues) {
self.init(positional: parsedValues.positional, options: parsedValues.options, flags: parsedValues.flags)
}
func positionalValue(at index: Int) -> String? {
guard index >= 0, index < self.positional.count else { return nil }
return self.positional[index]
}
func requiredPositional(_ index: Int, label: String) throws -> String {
guard let value = positionalValue(at: index) else {
throw CommanderBindingError.missingArgument(label: label)
}
return value
}
func singleOption(_ label: String) -> String? {
self.options[label]?.last
}
func optionValues(_ label: String) -> [String] {
self.options[label] ?? []
}
func flag(_ label: String) -> Bool {
self.flags.contains(label)
}
func decodePositional<T: ExpressibleFromArgument>(
_ index: Int,
label: String,
as type: T.Type = T.self
) throws -> T {
let raw = try requiredPositional(index, label: label)
guard let value = T(argument: raw) else {
throw CommanderBindingError.invalidArgument(label: label, value: raw, reason: "Unable to parse \(T.self)")
}
return value
}
func decodeOptionalPositional<T: ExpressibleFromArgument>(
_ index: Int,
label: String,
as type: T.Type = T.self
) throws -> T? {
guard let raw = positionalValue(at: index) else {
return nil
}
guard let value = T(argument: raw) else {
throw CommanderBindingError.invalidArgument(label: label, value: raw, reason: "Unable to parse \(T.self)")
}
return value
}
func decodeOption<T: ExpressibleFromArgument>(_ label: String, as type: T.Type = T.self) throws -> T? {
guard let raw = singleOption(label) else {
return nil
}
guard let value = T(argument: raw) else {
throw CommanderBindingError.invalidArgument(label: label, value: raw, reason: "Unable to parse \(T.self)")
}
return value
}
func requireOption<T: ExpressibleFromArgument>(_ label: String, as type: T.Type = T.self) throws -> T {
guard let value: T = try decodeOption(label, as: type) else {
throw CommanderBindingError.missingArgument(label: label)
}
return value
}
func decodeOptionEnum<T: RawRepresentable>(
_ label: String,
as type: T.Type = T.self,
caseInsensitive: Bool = true
) throws -> T? where T.RawValue == String {
guard let raw = singleOption(label) else {
return nil
}
let candidate = caseInsensitive ? raw.lowercased() : raw
guard let value = T(rawValue: candidate) else {
throw CommanderBindingError.invalidArgument(label: label, value: raw, reason: "Unknown value for \(T.self)")
}
return value
}
}
extension CommanderBindableValues {
func makeWindowOptions() throws -> WindowIdentificationOptions {
var options = WindowIdentificationOptions()
try fillWindowOptions(into: &options)
return options
}
func fillWindowOptions(into options: inout WindowIdentificationOptions) throws {
options.app = self.singleOption("app")
if let pid: Int32 = try decodeOption("pid", as: Int32.self) {
options.pid = pid
}
options.windowTitle = self.singleOption("windowTitle")
if let index: Int = try decodeOption("windowIndex", as: Int.self) {
options.windowIndex = index
}
}
func makeFocusOptions() throws -> FocusCommandOptions {
var options = FocusCommandOptions()
try fillFocusOptions(into: &options)
return options
}
func fillFocusOptions(into options: inout FocusCommandOptions) throws {
options.noAutoFocus = self.flag("noAutoFocus")
options.spaceSwitch = self.flag("spaceSwitch")
options.bringToCurrentSpace = self.flag("bringToCurrentSpace")
if let timeout: TimeInterval = try decodeOption("focusTimeoutSeconds", as: TimeInterval.self) {
options.focusTimeoutSeconds = timeout
}
if let retries: Int = try decodeOption("focusRetryCountValue", as: Int.self) {
options.focusRetryCountValue = retries
}
}
}
@MainActor
protocol CommanderBindableCommand {
mutating func applyCommanderValues(_ values: CommanderBindableValues) throws
}
enum CommanderBindingError: LocalizedError, Sendable, Equatable {
case missingArgument(label: String)
case invalidArgument(label: String, value: String, reason: String)
var errorDescription: String? {
switch self {
case let .missingArgument(label):
"Missing argument: \(label)"
case let .invalidArgument(label, value, reason):
"Invalid value '\(value)' for \(label): \(reason)"
}
}
}