diff --git a/Sources/tart/Commands/Clone.swift b/Sources/tart/Commands/Clone.swift index 67068b3a..ab72d8c1 100644 --- a/Sources/tart/Commands/Clone.swift +++ b/Sources/tart/Commands/Clone.swift @@ -18,7 +18,7 @@ struct Clone: AsyncParsableCommand { """ ) - @Argument(help: "source VM name") + @Argument(help: "source VM name", completion: .custom(completeMachines)) var sourceName: String @Argument(help: "new VM name") diff --git a/Sources/tart/Commands/Delete.swift b/Sources/tart/Commands/Delete.swift index 4f8308f2..366985f2 100644 --- a/Sources/tart/Commands/Delete.swift +++ b/Sources/tart/Commands/Delete.swift @@ -5,7 +5,7 @@ import SwiftUI struct Delete: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Delete a VM") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeMachines)) var name: [String] func run() async throws { diff --git a/Sources/tart/Commands/Export.swift b/Sources/tart/Commands/Export.swift index f0b5dcae..5e70ae8a 100644 --- a/Sources/tart/Commands/Export.swift +++ b/Sources/tart/Commands/Export.swift @@ -4,7 +4,7 @@ import Foundation struct Export: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Export VM to a compressed .tvm file") - @Argument(help: "Source VM name.") + @Argument(help: "Source VM name.", completion: .custom(completeMachines)) var name: String @Argument(help: "Path to the destination file.") diff --git a/Sources/tart/Commands/FQN.swift b/Sources/tart/Commands/FQN.swift index 0fd7f060..eb1e5816 100644 --- a/Sources/tart/Commands/FQN.swift +++ b/Sources/tart/Commands/FQN.swift @@ -5,7 +5,7 @@ import SystemConfiguration struct FQN: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Get a fully-qualified VM name", shouldDisplay: false) - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeMachines)) var name: String func run() async throws { diff --git a/Sources/tart/Commands/Get.swift b/Sources/tart/Commands/Get.swift index 733d8ff5..1dba89d7 100644 --- a/Sources/tart/Commands/Get.swift +++ b/Sources/tart/Commands/Get.swift @@ -15,7 +15,7 @@ fileprivate struct VMInfo: Encodable { struct Get: AsyncParsableCommand { static var configuration = CommandConfiguration(commandName: "get", abstract: "Get a VM's configuration") - @Argument(help: "VM name.") + @Argument(help: "VM name.", completion: .custom(completeLocalMachines)) var name: String @Option(help: "Output format: text or json") diff --git a/Sources/tart/Commands/IP.swift b/Sources/tart/Commands/IP.swift index 662d49e2..f271c6dc 100644 --- a/Sources/tart/Commands/IP.swift +++ b/Sources/tart/Commands/IP.swift @@ -13,7 +13,7 @@ enum IPResolutionStrategy: String, ExpressibleByArgument, CaseIterable { struct IP: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Get VM's IP address") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeLocalMachines)) var name: String @Option(help: "Number of seconds to wait for a potential VM booting") @@ -61,7 +61,7 @@ struct IP: AsyncParsableCommand { return ip } case .dhcp: - if let leases = try Leases(), let ip = try leases.ResolveMACAddress(macAddress: vmMACAddress) { + if let leases = try Leases(), let ip = leases.ResolveMACAddress(macAddress: vmMACAddress) { return ip } } diff --git a/Sources/tart/Commands/Push.swift b/Sources/tart/Commands/Push.swift index eb418366..a8b5aa12 100644 --- a/Sources/tart/Commands/Push.swift +++ b/Sources/tart/Commands/Push.swift @@ -6,7 +6,7 @@ import Compression struct Push: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Push a VM to a registry") - @Argument(help: "local or remote VM name") + @Argument(help: "local or remote VM name", completion: .custom(completeMachines)) var localName: String @Argument(help: "remote VM name(s)") diff --git a/Sources/tart/Commands/Rename.swift b/Sources/tart/Commands/Rename.swift index 1f2c0680..70ae351f 100644 --- a/Sources/tart/Commands/Rename.swift +++ b/Sources/tart/Commands/Rename.swift @@ -4,7 +4,7 @@ import Foundation struct Rename: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Rename a VM") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeLocalMachines)) var name: String @Argument(help: "new VM name") diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index a8ecc78b..9369b75e 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -14,7 +14,7 @@ struct IPNotFound: Error { struct Run: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Run a VM") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeLocalMachines)) var name: String @Flag(help: ArgumentHelp( @@ -54,7 +54,7 @@ struct Run: AsyncParsableCommand { #if arch(arm64) @Flag(help: ArgumentHelp( - "Use Virtualization.Framework's VNC server instead of the build-in UI.", + "Use Virtualization.Framework's VNC server instead of the built-in UI.", discussion: "Useful since this type of VNC is available in recovery mode and in macOS installation.\n" + "Note that this feature is experimental and there may be bugs present when using VNC.")) #endif diff --git a/Sources/tart/Commands/Set.swift b/Sources/tart/Commands/Set.swift index 0f69abc2..bfaceee4 100644 --- a/Sources/tart/Commands/Set.swift +++ b/Sources/tart/Commands/Set.swift @@ -4,7 +4,7 @@ import Foundation struct Set: AsyncParsableCommand { static var configuration = CommandConfiguration(commandName: "set", abstract: "Modify VM's configuration") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeLocalMachines)) var name: String @Option(help: "Number of VM CPUs") diff --git a/Sources/tart/Commands/Stop.swift b/Sources/tart/Commands/Stop.swift index 631bee9f..2c6a896f 100644 --- a/Sources/tart/Commands/Stop.swift +++ b/Sources/tart/Commands/Stop.swift @@ -6,7 +6,7 @@ import SwiftDate struct Stop: AsyncParsableCommand { static var configuration = CommandConfiguration(commandName: "stop", abstract: "Stop a VM") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeRunningMachines)) var name: String @Option(name: [.short, .long], help: "Seconds to wait for graceful termination before forcefully terminating the VM") diff --git a/Sources/tart/Commands/Suspend.swift b/Sources/tart/Commands/Suspend.swift index b476240f..b38fc3b8 100644 --- a/Sources/tart/Commands/Suspend.swift +++ b/Sources/tart/Commands/Suspend.swift @@ -6,7 +6,7 @@ import SwiftDate struct Suspend: AsyncParsableCommand { static var configuration = CommandConfiguration(commandName: "suspend", abstract: "Suspend a VM") - @Argument(help: "VM name") + @Argument(help: "VM name", completion: .custom(completeRunningMachines)) var name: String func run() async throws { diff --git a/Sources/tart/ShellCompletions/ShellCompletions.swift b/Sources/tart/ShellCompletions/ShellCompletions.swift new file mode 100644 index 00000000..39aa3c77 --- /dev/null +++ b/Sources/tart/ShellCompletions/ShellCompletions.swift @@ -0,0 +1,28 @@ +import Foundation + +fileprivate func normalizeName(_ name: String) -> String { + // Colons are misinterpreted by Zsh completion + return name.replacingOccurrences(of: ":", with: "\\:") +} + +func completeMachines(_ arguments: [String]) -> [String] { + let localVMs = (try? VMStorageLocal().list().map { name, _ in + normalizeName(name) + }) ?? [] + let ociVMs = (try? VMStorageOCI().list().map { name, _, _ in + normalizeName(name) + }) ?? [] + return (localVMs + ociVMs) +} + +func completeLocalMachines(_ arguments: [String]) -> [String] { + let localVMs = (try? VMStorageLocal().list()) ?? [] + return localVMs.map { name, _ in normalizeName(name) } +} + +func completeRunningMachines(_ arguments: [String]) -> [String] { + let localVMs = (try? VMStorageLocal().list()) ?? [] + return localVMs + .filter { _, vmDir in (try? vmDir.state() == .Running) ?? false} + .map { name, _ in normalizeName(name) } +}