Skip to content

Commit 082f617

Browse files
committed
Add CLI Helpers
1 parent c3d83ec commit 082f617

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import Foundation
2+
import ConsoleKit
3+
4+
public struct Console {
5+
6+
let terminal = Terminal()
7+
8+
@discardableResult public func heading(_ message: String, underline: String = "=") -> Self {
9+
self.terminal.output(message, style: .plain)
10+
self.terminal.output(String.init(repeating: underline, count: message.count), style: .plain)
11+
return self
12+
}
13+
14+
@discardableResult public func success(_ message: String) -> Self {
15+
self.terminal.success(message)
16+
return self
17+
}
18+
19+
@discardableResult public func error(_ message: String) -> Self {
20+
self.terminal.error(message)
21+
return self
22+
}
23+
24+
@discardableResult public func warn(_ message: String) -> Self {
25+
self.terminal.warning(message)
26+
return self
27+
}
28+
29+
@discardableResult public func info(_ message: String) -> Self {
30+
self.terminal.info(message)
31+
return self
32+
}
33+
34+
@discardableResult public func log(_ message: String) -> Self {
35+
self.terminal.print(message)
36+
return self
37+
}
38+
39+
@discardableResult func message(_ message: String, style: ConsoleStyle) -> Self {
40+
self.terminal.output(message, style: style)
41+
return self
42+
}
43+
44+
@discardableResult public func printList(_ list: [String], title: String) -> Self {
45+
self.terminal.print(title)
46+
47+
guard !list.isEmpty else {
48+
self.terminal.print(" [Empty]")
49+
return self
50+
}
51+
52+
for item in list {
53+
self.terminal.print(" " + item)
54+
}
55+
56+
return self
57+
}
58+
59+
@discardableResult public func printTable(
60+
data: Table,
61+
columnTitles: [String] = [],
62+
columnSeparator: String = " "
63+
) -> Self {
64+
65+
if data.isEmpty {
66+
self.terminal.print("[Empty]")
67+
return self
68+
}
69+
70+
// Prepend the Column Titles, if present
71+
let table = columnTitles.isEmpty ? data : [columnTitles] + data
72+
73+
let columnCount = columnCounts(for: table)
74+
75+
for row in table {
76+
let string = zip(row, columnCount).map(self.padString).joined(separator: columnSeparator)
77+
self.terminal.print(string)
78+
}
79+
80+
return self
81+
}
82+
}
83+
84+
public struct ProgressBar {
85+
86+
private let terminal = Terminal()
87+
private let startDate = Date()
88+
89+
init(title: String) {
90+
terminal.info(title)
91+
terminal.print() // Deliberately empty string
92+
}
93+
94+
public static func start(title: String) -> Self {
95+
return ProgressBar(title: title)
96+
}
97+
98+
public func update(_ progress: FileTransferProgress) {
99+
terminal.clear(lines: 1)
100+
101+
let elapsedTime = Date().timeIntervalSince(startDate)
102+
103+
let rate = progress.dataRate(timeIntervalSinceStart: elapsedTime)
104+
let remaining = progress.estimatedTimeRemaining(timeIntervalSinceStart: elapsedTime)
105+
106+
terminal.print("\(progress.formattedPercentage)% [\(rate)/s, \(Format.time(remaining))]")
107+
}
108+
}
109+
110+
// MARK: Static Helpers
111+
extension Console {
112+
public static func startProgress(_ string: String) -> ProgressBar {
113+
ProgressBar(title: string)
114+
}
115+
116+
public static func startImageDownload(_ image: RemoteVMImage) -> ProgressBar {
117+
let size = Format.fileBytes(image.imageObject.size)
118+
return ProgressBar(title: "Downloading \(image.fileName) (\(size))")
119+
}
120+
121+
public static func startFileDownload(_ file: S3Object) -> ProgressBar {
122+
let size = Format.fileBytes(file.size)
123+
return ProgressBar(title: "Downloading \(file.key) (\(size))")
124+
}
125+
}
126+
127+
// MARK: Static Initializers
128+
extension Console {
129+
@discardableResult public static func heading(_ message: String) -> Self {
130+
return Console().heading(message)
131+
}
132+
133+
@discardableResult public static func success(_ message: String) -> Self {
134+
return Console().success(message)
135+
}
136+
137+
@discardableResult public static func error(_ message: String) -> Self {
138+
return Console().error(message)
139+
}
140+
141+
@discardableResult public static func warn(_ message: String) -> Self {
142+
return Console().warn(message)
143+
}
144+
145+
@discardableResult public static func info(_ message: String) -> Self {
146+
return Console().info(message)
147+
}
148+
149+
@discardableResult public static func log(_ message: String) -> Self {
150+
return Console().log(message)
151+
}
152+
153+
@discardableResult public static func printList(_ list: [String], title: String) -> Self {
154+
return Console().printList(list, title: title)
155+
}
156+
157+
@discardableResult public static func printTable(data: Table, columnTitles: [String] = []) -> Self {
158+
return Console().printTable(data: data, columnTitles: columnTitles)
159+
}
160+
161+
public static func crash(message: String, reason error: ExitCode) -> Never {
162+
Console().error(message)
163+
Foundation.exit(error.rawValue)
164+
}
165+
166+
public static func exit(message: String = "", style: ConsoleStyle = .plain) -> Never {
167+
Console().message(message, style: style)
168+
Foundation.exit(0)
169+
}
170+
}
171+
172+
// MARK: Table Support
173+
extension Console {
174+
public typealias Table = [[String]]
175+
176+
func columnCounts(for table: Table) -> [Int] {
177+
transpose(matrix: table).map { $0.map(\.count).max() ?? 0 }
178+
}
179+
180+
func transpose(matrix: Table) -> Table {
181+
guard let numberOfColumns = matrix.first?.count else {
182+
return matrix
183+
}
184+
185+
var newTable = [[String]](repeating: [String](repeating: "", count: matrix.count), count: numberOfColumns)
186+
187+
for (rowIndex, row) in matrix.enumerated() {
188+
for (colIndex, col) in row.enumerated() {
189+
newTable[colIndex][rowIndex] = col
190+
}
191+
}
192+
193+
return newTable
194+
}
195+
196+
func padString(_ string: String, toLength length: Int) -> String {
197+
string.padding(toLength: length, withPad: " ", startingAt: 0)
198+
}
199+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
public enum ExitCode: Int32, Error {
4+
case fileNotFound
5+
case unableToFindRemoteImage
6+
case unableToImportVM
7+
case invalidVMStatus
8+
case notEnoughLocalDiskSpace
9+
case parallelsVirtualMachineDoesNotExist
10+
case parallelsVirtualMachineIsNotStopped
11+
case parallelsVirtualMachineAlreadyExists
12+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Foundation
2+
3+
public struct Format {
4+
5+
public static func fileBytes(_ count: Int) -> String {
6+
fileBytes(Int64(count))
7+
}
8+
9+
public static func fileBytes(_ count: Int64) -> String {
10+
let formatter = ByteCountFormatter()
11+
formatter.zeroPadsFractionDigits = true
12+
formatter.countStyle = .file
13+
return formatter.string(fromByteCount: count)
14+
}
15+
16+
public static func memoryBytes(_ count: UInt64) -> String {
17+
memoryBytes(Int64(count))
18+
}
19+
20+
public static func memoryBytes(_ count: Int64) -> String {
21+
let formatter = ByteCountFormatter()
22+
formatter.zeroPadsFractionDigits = true
23+
formatter.countStyle = .memory
24+
return formatter.string(fromByteCount: count)
25+
}
26+
27+
public static func time(_ interval: TimeInterval) -> String {
28+
let formatter = RelativeDateTimeFormatter()
29+
formatter.formattingContext = .standalone
30+
return formatter.localizedString(fromTimeInterval: interval)
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import XCTest
2+
@testable import libhostmgr
3+
4+
final class ConsoleTests: XCTestCase {
5+
6+
// MARK: Table Calculations
7+
func testThatColumnCountWorks() throws {
8+
let table = [["Jack", "Reacher"], ["James", "Bond"], ["Jason", "Bourne"]]
9+
XCTAssertEqual([5, 7], Console().columnCounts(for: table))
10+
}
11+
12+
func testThatTransposeWorks() throws {
13+
let original = [
14+
["Jack", "Reacher"],
15+
["James", "Bond"],
16+
["Jason", "Bourne"]
17+
]
18+
let transposed = [
19+
["Jack", "James", "Jason"],
20+
["Reacher", "Bond", "Bourne"]
21+
]
22+
XCTAssertEqual(transposed, Console().transpose(matrix: original))
23+
}
24+
}

0 commit comments

Comments
 (0)