remove swift-argument-parser - it was adding well over 1Mb of bloat to release builds
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"originHash" : "5f2b81278809343fed36ed8c17e7d6930bfd5b85261cdf5dadb17ab7ffdfc0e3",
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "swift-argument-parser",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b",
|
|
||||||
"version" : "1.5.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 3
|
|
||||||
}
|
|
||||||
+1
-11
@@ -8,17 +8,7 @@ let package = Package(
|
|||||||
platforms: [
|
platforms: [
|
||||||
.macOS("14.2")
|
.macOS("14.2")
|
||||||
],
|
],
|
||||||
dependencies: [
|
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0")
|
|
||||||
],
|
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
.executableTarget(name: "audiotee")
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
|
||||||
.executableTarget(
|
|
||||||
name: "audiotee",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Error Types
|
||||||
|
|
||||||
|
enum ArgumentParserError: Error, CustomStringConvertible {
|
||||||
|
case unknownOption(String)
|
||||||
|
case missingValue(String)
|
||||||
|
case invalidValue(String, String)
|
||||||
|
case validationFailed(String)
|
||||||
|
case helpRequested
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .unknownOption(let option):
|
||||||
|
return "Unknown option: \(option)"
|
||||||
|
case .missingValue(let option):
|
||||||
|
return "Missing value for option: \(option)"
|
||||||
|
case .invalidValue(let option, let value):
|
||||||
|
return "Invalid value '\(value)' for option: \(option)"
|
||||||
|
case .validationFailed(let message):
|
||||||
|
return message
|
||||||
|
case .helpRequested:
|
||||||
|
return "" // Help is handled separately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Argument Configuration
|
||||||
|
|
||||||
|
struct ArgumentConfig {
|
||||||
|
let name: String
|
||||||
|
let shortName: String?
|
||||||
|
let help: String
|
||||||
|
let isFlag: Bool
|
||||||
|
let isArray: Bool
|
||||||
|
let defaultValue: String?
|
||||||
|
|
||||||
|
init(
|
||||||
|
name: String, shortName: String? = nil, help: String, isFlag: Bool = false,
|
||||||
|
isArray: Bool = false, defaultValue: String? = nil
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.shortName = shortName
|
||||||
|
self.help = help
|
||||||
|
self.isFlag = isFlag
|
||||||
|
self.isArray = isArray
|
||||||
|
self.defaultValue = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Simple Argument Parser
|
||||||
|
|
||||||
|
class SimpleArgumentParser {
|
||||||
|
private let programName: String
|
||||||
|
private let abstract: String
|
||||||
|
private let discussion: String
|
||||||
|
private var configs: [ArgumentConfig] = []
|
||||||
|
private var parsedValues: [String: [String]] = [:]
|
||||||
|
|
||||||
|
init(programName: String, abstract: String, discussion: String = "") {
|
||||||
|
self.programName = programName
|
||||||
|
self.abstract = abstract
|
||||||
|
self.discussion = discussion
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOption(name: String, shortName: String? = nil, help: String, defaultValue: String? = nil)
|
||||||
|
{
|
||||||
|
configs.append(
|
||||||
|
ArgumentConfig(name: name, shortName: shortName, help: help, defaultValue: defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addArrayOption(name: String, shortName: String? = nil, help: String) {
|
||||||
|
configs.append(ArgumentConfig(name: name, shortName: shortName, help: help, isArray: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFlag(name: String, shortName: String? = nil, help: String) {
|
||||||
|
configs.append(ArgumentConfig(name: name, shortName: shortName, help: help, isFlag: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(_ arguments: [String] = Array(CommandLine.arguments.dropFirst())) throws {
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
while i < arguments.count {
|
||||||
|
let arg = arguments[i]
|
||||||
|
|
||||||
|
if arg == "--help" || arg == "-h" {
|
||||||
|
throw ArgumentParserError.helpRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
guard arg.hasPrefix("-") else {
|
||||||
|
throw ArgumentParserError.unknownOption(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionName = findOptionName(arg)
|
||||||
|
guard let config = findConfig(optionName) else {
|
||||||
|
throw ArgumentParserError.unknownOption(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.isFlag {
|
||||||
|
parsedValues[config.name] = ["true"]
|
||||||
|
i += 1
|
||||||
|
} else {
|
||||||
|
// Need a value
|
||||||
|
i += 1
|
||||||
|
guard i < arguments.count else {
|
||||||
|
throw ArgumentParserError.missingValue(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.isArray {
|
||||||
|
// Collect all values until next option or end
|
||||||
|
var values: [String] = []
|
||||||
|
while i < arguments.count && !arguments[i].hasPrefix("-") {
|
||||||
|
values.append(arguments[i])
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if values.isEmpty {
|
||||||
|
throw ArgumentParserError.missingValue(arg)
|
||||||
|
}
|
||||||
|
parsedValues[config.name] = values
|
||||||
|
} else {
|
||||||
|
let value = arguments[i]
|
||||||
|
parsedValues[config.name] = [value]
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values for missing options
|
||||||
|
for config in configs {
|
||||||
|
if parsedValues[config.name] == nil, let defaultValue = config.defaultValue {
|
||||||
|
parsedValues[config.name] = [defaultValue]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findOptionName(_ arg: String) -> String {
|
||||||
|
if arg.hasPrefix("--") {
|
||||||
|
return String(arg.dropFirst(2))
|
||||||
|
} else if arg.hasPrefix("-") {
|
||||||
|
return String(arg.dropFirst(1))
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findConfig(_ optionName: String) -> ArgumentConfig? {
|
||||||
|
return configs.first { config in
|
||||||
|
config.name == optionName || config.shortName == optionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValue<T>(_ name: String, as type: T.Type) throws -> T {
|
||||||
|
guard let values = parsedValues[name], let value = values.first else {
|
||||||
|
throw ArgumentParserError.missingValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try convertValue(value, to: type, optionName: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOptionalValue<T>(_ name: String, as type: T.Type) throws -> T? {
|
||||||
|
guard let values = parsedValues[name], let value = values.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return try convertValue(value, to: type, optionName: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArrayValue<T>(_ name: String, as type: T.Type) throws -> [T] {
|
||||||
|
guard let values = parsedValues[name] else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return try values.map { try convertValue($0, to: type, optionName: name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFlag(_ name: String) -> Bool {
|
||||||
|
return parsedValues[name]?.first == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertValue<T>(_ value: String, to type: T.Type, optionName: String) throws -> T {
|
||||||
|
if type == String.self {
|
||||||
|
return value as! T
|
||||||
|
} else if type == Int32.self {
|
||||||
|
guard let intValue = Int32(value) else {
|
||||||
|
throw ArgumentParserError.invalidValue(optionName, value)
|
||||||
|
}
|
||||||
|
return intValue as! T
|
||||||
|
} else if type == Double.self {
|
||||||
|
guard let doubleValue = Double(value) else {
|
||||||
|
throw ArgumentParserError.invalidValue(optionName, value)
|
||||||
|
}
|
||||||
|
return doubleValue as! T
|
||||||
|
} else if type == OutputFormat.self {
|
||||||
|
guard let format = OutputFormat(rawValue: value) else {
|
||||||
|
throw ArgumentParserError.invalidValue(optionName, value)
|
||||||
|
}
|
||||||
|
return format as! T
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ArgumentParserError.invalidValue(optionName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
print(abstract)
|
||||||
|
|
||||||
|
if !discussion.isEmpty {
|
||||||
|
print("\n\(discussion)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\nUSAGE:")
|
||||||
|
print(" \(programName) [OPTIONS]")
|
||||||
|
|
||||||
|
let optionConfigs = configs.filter { !$0.isFlag }
|
||||||
|
let flagConfigs = configs.filter { $0.isFlag }
|
||||||
|
|
||||||
|
if !optionConfigs.isEmpty {
|
||||||
|
print("\nOPTIONS:")
|
||||||
|
for config in optionConfigs {
|
||||||
|
let shortName = config.shortName.map { "-\($0), " } ?? ""
|
||||||
|
let defaultDesc = config.defaultValue.map { " (default: \($0))" } ?? ""
|
||||||
|
print(" \(shortName)--\(config.name) \(config.help)\(defaultDesc)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flagConfigs.isEmpty {
|
||||||
|
print("\nFLAGS:")
|
||||||
|
for config in flagConfigs {
|
||||||
|
let shortName = config.shortName.map { "-\($0), " } ?? ""
|
||||||
|
print(" \(shortName)--\(config.name) \(config.help)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n -h, --help Show this help message")
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-25
@@ -1,9 +1,19 @@
|
|||||||
import ArgumentParser
|
|
||||||
import CoreAudio
|
import CoreAudio
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct AudioTee: ParsableCommand {
|
struct AudioTee {
|
||||||
static let configuration = CommandConfiguration(
|
var format: OutputFormat = .auto
|
||||||
|
var includeProcesses: [Int32] = []
|
||||||
|
var excludeProcesses: [Int32] = []
|
||||||
|
var mute: Bool = false
|
||||||
|
var sampleRate: Double?
|
||||||
|
var chunkDuration: Double = 0.2
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
static func main() {
|
||||||
|
let parser = SimpleArgumentParser(
|
||||||
|
programName: "audiotee",
|
||||||
abstract: "Capture system audio and stream to stdout",
|
abstract: "Capture system audio and stream to stdout",
|
||||||
discussion: """
|
discussion: """
|
||||||
AudioTee captures system audio using Core Audio taps and streams it as structured output.
|
AudioTee captures system audio using Core Audio taps and streams it as structured output.
|
||||||
@@ -31,33 +41,60 @@ struct AudioTee: ParsableCommand {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@Option(name: .shortAndLong, help: "Output format")
|
// Configure arguments
|
||||||
var format: OutputFormat = .auto
|
parser.addOption(name: "format", shortName: "f", help: "Output format", defaultValue: "auto")
|
||||||
|
parser.addArrayOption(
|
||||||
@Option(
|
name: "include-processes",
|
||||||
name: .long, help: "Process IDs to include (space-separated, empty = all processes)")
|
help: "Process IDs to include (space-separated, empty = all processes)")
|
||||||
var includeProcesses: [Int32] = []
|
parser.addArrayOption(
|
||||||
|
name: "exclude-processes", help: "Process IDs to exclude (space-separated)")
|
||||||
@Option(
|
parser.addFlag(name: "mute", help: "Mute processes being tapped")
|
||||||
name: .long, help: "Process IDs to exclude (space-separated)")
|
parser.addOption(
|
||||||
var excludeProcesses: [Int32] = []
|
name: "sample-rate",
|
||||||
|
|
||||||
@Flag(name: .long, help: "Mute processes being tapped")
|
|
||||||
var mute: Bool = false
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
name: .long,
|
|
||||||
help: "Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)")
|
help: "Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)")
|
||||||
var sampleRate: Double?
|
parser.addOption(
|
||||||
|
name: "chunk-duration", help: "Audio chunk duration in seconds", defaultValue: "0.2")
|
||||||
|
|
||||||
@Option(
|
// Parse arguments
|
||||||
name: .long,
|
do {
|
||||||
help: "Audio chunk duration in seconds (default: 0.2)")
|
try parser.parse()
|
||||||
var chunkDuration: Double = 0.2
|
|
||||||
|
var audioTee = AudioTee()
|
||||||
|
|
||||||
|
// Extract values
|
||||||
|
audioTee.format = try parser.getValue("format", as: OutputFormat.self)
|
||||||
|
audioTee.includeProcesses = try parser.getArrayValue("include-processes", as: Int32.self)
|
||||||
|
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
|
||||||
|
audioTee.mute = parser.getFlag("mute")
|
||||||
|
audioTee.sampleRate = try parser.getOptionalValue("sample-rate", as: Double.self)
|
||||||
|
audioTee.chunkDuration = try parser.getValue("chunk-duration", as: Double.self)
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
try audioTee.validate()
|
||||||
|
|
||||||
|
// Run
|
||||||
|
try audioTee.run()
|
||||||
|
|
||||||
|
} catch ArgumentParserError.helpRequested {
|
||||||
|
parser.printHelp()
|
||||||
|
exit(0)
|
||||||
|
} catch ArgumentParserError.validationFailed(let message) {
|
||||||
|
print("Error: \(message)", to: &standardError)
|
||||||
|
exit(1)
|
||||||
|
} catch let error as ArgumentParserError {
|
||||||
|
print("Error: \(error.description)", to: &standardError)
|
||||||
|
parser.printHelp()
|
||||||
|
exit(1)
|
||||||
|
} catch {
|
||||||
|
print("Error: \(error)", to: &standardError)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validate() throws {
|
func validate() throws {
|
||||||
if !includeProcesses.isEmpty && !excludeProcesses.isEmpty {
|
if !includeProcesses.isEmpty && !excludeProcesses.isEmpty {
|
||||||
throw ValidationError("Cannot specify both --include-processes and --exclude-processes")
|
throw ArgumentParserError.validationFailed(
|
||||||
|
"Cannot specify both --include-processes and --exclude-processes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,3 +196,27 @@ struct AudioTee: ParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper for stderr output
|
||||||
|
var standardError = FileHandle.standardError
|
||||||
|
|
||||||
|
extension FileHandle: @retroactive TextOutputStream {
|
||||||
|
public func write(_ string: String) {
|
||||||
|
let data = Data(string.utf8)
|
||||||
|
self.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit code handling
|
||||||
|
enum ExitCode: Error {
|
||||||
|
case failure
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExitCode {
|
||||||
|
var code: Int32 {
|
||||||
|
switch self {
|
||||||
|
case .failure:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import ArgumentParser
|
enum OutputFormat: String, CaseIterable {
|
||||||
|
|
||||||
enum OutputFormat: String, CaseIterable, ExpressibleByArgument {
|
|
||||||
case json = "json"
|
case json = "json"
|
||||||
case binary = "binary"
|
case binary = "binary"
|
||||||
case auto = "auto"
|
case auto = "auto"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import ArgumentParser
|
|
||||||
import CoreAudio
|
import CoreAudio
|
||||||
|
|
||||||
public enum TapMuteBehavior: String, CaseIterable, ExpressibleByArgument {
|
public enum TapMuteBehavior: String, CaseIterable {
|
||||||
case unmuted = "unmuted"
|
case unmuted = "unmuted"
|
||||||
case muted = "muted"
|
case muted = "muted"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import ArgumentParser
|
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user