Skip to content

Commit d51db64

Browse files
authored
Merge pull request #1074 from rgoldberg/1073-download-progress
Improve download progress output
2 parents a3df15a + 5e079cc commit d51db64

File tree

4 files changed

+80
-73
lines changed

4 files changed

+80
-73
lines changed

.swiftlint.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ disabled_rules:
3232
- strict_fileprivate
3333
- type_body_length
3434
- vertical_whitespace_between_cases
35-
- void_function_in_ternary
3635
attributes:
3736
always_on_line_above: ['@Flag', '@MainActor', '@OptionGroup', '@objc']
3837
cyclomatic_complexity:

Sources/mas/AppStore/DownloadQueueObserver.swift

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -44,40 +44,37 @@ final class DownloadQueueObserver: CKDownloadQueueObserver {
4444
return
4545
}
4646

47-
let appNameAndVersion = metadata.appNameAndVersion
48-
let currPhaseType = status.activePhase?.phaseType
47+
let currPhaseType = status.activePhaseType
4948
if prevPhaseType != currPhaseType {
5049
switch currPhaseType {
51-
case downloadingPhaseType:
52-
if prevPhaseType == initialPhaseType {
53-
MAS.printer.progress(status: status, for: appNameAndVersion)
50+
case .downloading:
51+
if prevPhaseType == .initial {
52+
MAS.printer.progress(phase: currPhaseType, for: metadata.appNameAndVersion)
5453
}
55-
case downloadedPhaseType:
56-
if prevPhaseType == downloadingPhaseType {
57-
MAS.printer.progress(status: status, for: appNameAndVersion)
54+
case .downloaded:
55+
if prevPhaseType == .downloading {
56+
MAS.printer.progress(phase: currPhaseType, for: metadata.appNameAndVersion)
5857
}
59-
case installingPhaseType:
60-
MAS.printer.progress(status: status, for: appNameAndVersion)
58+
case .installing:
59+
MAS.printer.progress(phase: currPhaseType, for: metadata.appNameAndVersion)
6160
default:
6261
break
6362
}
6463
}
6564

66-
if isatty(FileHandle.standardOutput.fileDescriptor) != 0 {
67-
// Only output the progress bar if connected to a terminal
68-
let percentComplete = min(
69-
status.percentComplete / (currPhaseType == downloadingPhaseType ? 0.8 : 0.2),
70-
1.0
71-
)
65+
let percentComplete = status.phasePercentComplete
66+
if isatty(FileHandle.standardOutput.fileDescriptor) != 0, percentComplete != 0 || currPhaseType != .initial {
67+
// Output the progress bar iff connected to a terminal
7268
let totalLength = 60
7369
let completedLength = Int(percentComplete * Float(totalLength))
74-
MAS.printer.ephemeral(
70+
MAS.printer.clearCurrentLine(of: .standardOutput)
71+
MAS.printer.info(
7572
String(repeating: "#", count: completedLength),
7673
String(repeating: "-", count: totalLength - completedLength),
7774
" ",
7875
String(format: "%.0f%%", floor(percentComplete * 100).rounded()),
7976
" ",
80-
status.activePhaseDescription,
77+
status.activePhaseType.description,
8178
separator: "",
8279
terminator: ""
8380
)
@@ -99,19 +96,29 @@ final class DownloadQueueObserver: CKDownloadQueueObserver {
9996
return
10097
}
10198

102-
MAS.printer.terminateEphemeral()
99+
MAS.printer.clearCurrentLine(of: .standardOutput)
103100

104-
guard !status.isFailed else {
105-
errorHandler?(status.error ?? MASError.runtimeError("Failed to download \(metadata.appNameAndVersion)"))
106-
return
107-
}
108-
guard !status.isCancelled else {
109-
shouldCancel(download, false) ? completionHandler?() : errorHandler?(MASError.runtimeError("Download cancelled"))
110-
return
111-
}
101+
do {
102+
if let error = status.error {
103+
throw error
104+
}
105+
guard !status.isFailed else {
106+
throw MASError.runtimeError("Failed to download \(metadata.appNameAndVersion)")
107+
}
108+
guard !status.isCancelled else {
109+
guard shouldCancel(download, false) else {
110+
throw MASError.runtimeError("Download cancelled")
111+
}
112112

113-
MAS.printer.notice("Installed", metadata.appNameAndVersion)
114-
completionHandler?()
113+
completionHandler?()
114+
return
115+
}
116+
117+
MAS.printer.notice("Installed", metadata.appNameAndVersion)
118+
completionHandler?()
119+
} catch {
120+
errorHandler?(error)
121+
}
115122
}
116123

117124
func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws {
@@ -135,12 +142,38 @@ final class DownloadQueueObserver: CKDownloadQueueObserver {
135142
}
136143
}
137144

138-
private typealias PhaseType = Int64
145+
private enum PhaseType: Int64 { // swiftlint:disable sorted_enum_cases
146+
case initial = 4 // swiftlint:disable:previous one_declaration_per_file
147+
case downloading = 0
148+
case downloaded = 5
149+
case installing = 1 // swiftlint:enable sorted_enum_cases
150+
}
151+
152+
extension PhaseType: CustomStringConvertible {
153+
var description: String {
154+
switch self {
155+
case .downloading:
156+
"Downloading"
157+
case .downloaded:
158+
"Downloaded"
159+
case .installing:
160+
"Installing"
161+
default:
162+
"Waiting"
163+
}
164+
}
165+
}
166+
167+
private extension PhaseType? {
168+
var description: String {
169+
map(\.description) ?? "Processing"
170+
}
171+
}
139172

140173
private extension Printer {
141-
func progress(status: SSDownloadStatus, for appNameAndVersion: String) {
142-
terminateEphemeral()
143-
notice(status.activePhaseDescription, appNameAndVersion)
174+
func progress(phase: PhaseType?, for appNameAndVersion: String) {
175+
clearCurrentLine(of: .standardOutput)
176+
notice(phase.description, appNameAndVersion)
144177
}
145178
}
146179

@@ -151,23 +184,7 @@ private extension SSDownloadMetadata {
151184
}
152185

153186
private extension SSDownloadStatus {
154-
var activePhaseDescription: String {
155-
switch activePhase?.phaseType {
156-
case downloadedPhaseType:
157-
"Downloaded"
158-
case downloadingPhaseType:
159-
"Downloading"
160-
case installingPhaseType:
161-
"Installing"
162-
case nil:
163-
"Processing"
164-
default:
165-
"Waiting"
166-
}
187+
var activePhaseType: PhaseType? {
188+
activePhase.flatMap { PhaseType(rawValue: $0.phaseType) }
167189
}
168190
}
169-
170-
private let downloadingPhaseType = 0 as PhaseType
171-
private let installingPhaseType = 1 as PhaseType
172-
private let initialPhaseType = 4 as PhaseType
173-
private let downloadedPhaseType = 5 as PhaseType

Sources/mas/Commands/Uninstall.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ extension MAS {
5555
private func uninstallingAppPathOrderedSet(from installedApps: [InstalledApp]) -> OrderedSet<String> {
5656
requiredAppIDsOptionGroup.appIDs.reduce(into: OrderedSet<String>()) { uninstallingAppPathSet, appID in
5757
let installedAppPaths = installedApps.filter { $0.matches(appID) }.map(\.path)
58-
installedAppPaths.isEmpty
59-
? printer.error(appID.notInstalledMessage) // swiftformat:disable:this indent
60-
: uninstallingAppPathSet.formUnion(installedAppPaths)
58+
guard !installedAppPaths.isEmpty else {
59+
printer.error(appID.notInstalledMessage)
60+
return
61+
}
62+
63+
uninstallingAppPathSet.formUnion(installedAppPaths)
6164
}
6265
}
6366
}

Sources/mas/Utilities/Printer.swift

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,6 @@ struct Printer {
2828
print(items.map(String.init(describing:)), separator: separator, terminator: terminator, to: .standardOutput)
2929
}
3030

31-
/// Clears current line from `stdout`, then prints to `stdout`, then flushes
32-
/// `stdout`.
33-
func ephemeral(_ items: Any..., separator: String = " ", terminator: String = "\n") {
34-
clearCurrentLine(of: .standardOutput)
35-
print(items.map(String.init(describing:)), separator: separator, terminator: terminator, to: .standardOutput)
36-
}
37-
38-
/// Clears current line of `stdout`.
39-
func terminateEphemeral() {
40-
clearCurrentLine(of: .standardOutput)
41-
}
42-
4331
/// Prints to `stdout`, prefixed with "==> "; if connected to a terminal, the
4432
/// prefix is blue.
4533
func notice(_ items: Any..., separator: String = " ", terminator: String = "\n") {
@@ -73,6 +61,12 @@ struct Printer {
7361
)
7462
}
7563

64+
func clearCurrentLine(of fileHandle: FileHandle) {
65+
if isatty(fileHandle.fileDescriptor) != 0 {
66+
fileHandle.write(Data("\(csi)2K\(csi)0G".utf8))
67+
}
68+
}
69+
7670
private func errorTerminator(_ items: Any..., error: (any Error)?, terminator: String) -> String {
7771
error.map { error in
7872
let errorDescription = String(describing: error)
@@ -102,12 +96,6 @@ struct Printer {
10296
to: fileHandle
10397
)
10498
}
105-
106-
private func clearCurrentLine(of fileHandle: FileHandle) {
107-
if isatty(fileHandle.fileDescriptor) != 0 {
108-
fileHandle.write(Data("\(csi)2K\(csi)0G".utf8))
109-
}
110-
}
11199
}
112100

113101
/// Terminal Control Sequence Indicator.

0 commit comments

Comments
 (0)