Skip to content

Commit 78f858a

Browse files
rauhulMaxDesiatov
andauthored
Move and refactor ProgressAnimation code from TSC (#7328)
SwiftPM uses ProgressAnimation from TSCUtilities in a variety of inconsistent manners across various swift-something commands. This commit replaces the uses of ProgressAnimation throughout SwiftPM with common entrypoints and lays the ground work for creating multi-line parallel progress animations as seen in tools like `bazel build` and `docker build`. --------- Co-authored-by: Max Desiatov <[email protected]>
1 parent ec4bc27 commit 78f858a

16 files changed

+424
-54
lines changed

Sources/Basics/CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ add_library(Basics
5151
Netrc.swift
5252
Observability.swift
5353
OSSignpost.swift
54-
ProgressAnimation.swift
54+
ProgressAnimation/NinjaProgressAnimation.swift
55+
ProgressAnimation/PercentProgressAnimation.swift
56+
ProgressAnimation/ProgressAnimationProtocol.swift
57+
ProgressAnimation/SingleLinePercentProgressAnimation.swift
58+
ProgressAnimation/ThrottledProgressAnimation.swift
5559
SQLite.swift
5660
Sandbox.swift
5761
SendableTimeInterval.swift
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import class TSCBasic.TerminalController
14+
import protocol TSCBasic.WritableByteStream
15+
16+
extension ProgressAnimation {
17+
/// A ninja-like progress animation that adapts to the provided output stream.
18+
@_spi(SwiftPMInternal)
19+
public static func ninja(
20+
stream: WritableByteStream,
21+
verbose: Bool
22+
) -> any ProgressAnimationProtocol {
23+
Self.dynamic(
24+
stream: stream,
25+
verbose: verbose,
26+
ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0) },
27+
dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: nil) },
28+
defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream) }
29+
)
30+
}
31+
}
32+
33+
/// A redrawing ninja-like progress animation.
34+
final class RedrawingNinjaProgressAnimation: ProgressAnimationProtocol {
35+
private let terminal: TerminalController
36+
private var hasDisplayedProgress = false
37+
38+
init(terminal: TerminalController) {
39+
self.terminal = terminal
40+
}
41+
42+
func update(step: Int, total: Int, text: String) {
43+
assert(step <= total)
44+
45+
terminal.clearLine()
46+
47+
let progressText = "[\(step)/\(total)] \(text)"
48+
let width = terminal.width
49+
if progressText.utf8.count > width {
50+
let suffix = ""
51+
terminal.write(String(progressText.prefix(width - suffix.utf8.count)))
52+
terminal.write(suffix)
53+
} else {
54+
terminal.write(progressText)
55+
}
56+
57+
hasDisplayedProgress = true
58+
}
59+
60+
func complete(success: Bool) {
61+
if hasDisplayedProgress {
62+
terminal.endLine()
63+
}
64+
}
65+
66+
func clear() {
67+
terminal.clearLine()
68+
}
69+
}
70+
71+
/// A multi-line ninja-like progress animation.
72+
final class MultiLineNinjaProgressAnimation: ProgressAnimationProtocol {
73+
private struct Info: Equatable {
74+
let step: Int
75+
let total: Int
76+
let text: String
77+
}
78+
79+
private let stream: WritableByteStream
80+
private var lastDisplayedText: String? = nil
81+
82+
init(stream: WritableByteStream) {
83+
self.stream = stream
84+
}
85+
86+
func update(step: Int, total: Int, text: String) {
87+
assert(step <= total)
88+
89+
guard text != lastDisplayedText else { return }
90+
91+
stream.send("[\(step)/\(total)] ").send(text)
92+
stream.send("\n")
93+
stream.flush()
94+
lastDisplayedText = text
95+
}
96+
97+
func complete(success: Bool) {
98+
}
99+
100+
func clear() {
101+
}
102+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import class TSCBasic.TerminalController
14+
import protocol TSCBasic.WritableByteStream
15+
16+
extension ProgressAnimation {
17+
/// A percent-based progress animation that adapts to the provided output stream.
18+
@_spi(SwiftPMInternal)
19+
public static func percent(
20+
stream: WritableByteStream,
21+
verbose: Bool,
22+
header: String
23+
) -> any ProgressAnimationProtocol {
24+
Self.dynamic(
25+
stream: stream,
26+
verbose: verbose,
27+
ttyTerminalAnimationFactory: { RedrawingPercentProgressAnimation(terminal: $0, header: header) },
28+
dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: header) },
29+
defaultAnimationFactory: { MultiLinePercentProgressAnimation(stream: stream, header: header) }
30+
)
31+
}
32+
}
33+
34+
/// A redrawing lit-like progress animation.
35+
final class RedrawingPercentProgressAnimation: ProgressAnimationProtocol {
36+
private let terminal: TerminalController
37+
private let header: String
38+
private var hasDisplayedHeader = false
39+
40+
init(terminal: TerminalController, header: String) {
41+
self.terminal = terminal
42+
self.header = header
43+
}
44+
45+
/// Creates repeating string for count times.
46+
/// If count is negative, returns empty string.
47+
private func repeating(string: String, count: Int) -> String {
48+
return String(repeating: string, count: max(count, 0))
49+
}
50+
51+
func update(step: Int, total: Int, text: String) {
52+
assert(step <= total)
53+
54+
let width = terminal.width
55+
if !hasDisplayedHeader {
56+
let spaceCount = width / 2 - header.utf8.count / 2
57+
terminal.write(repeating(string: " ", count: spaceCount))
58+
terminal.write(header, inColor: .cyan, bold: true)
59+
terminal.endLine()
60+
hasDisplayedHeader = true
61+
} else {
62+
terminal.moveCursor(up: 1)
63+
}
64+
65+
terminal.clearLine()
66+
let percentage = step * 100 / total
67+
let paddedPercentage = percentage < 10 ? " \(percentage)" : "\(percentage)"
68+
let prefix = "\(paddedPercentage)% " + terminal.wrap("[", inColor: .green, bold: true)
69+
terminal.write(prefix)
70+
71+
let barWidth = width - prefix.utf8.count
72+
let n = Int(Double(barWidth) * Double(percentage) / 100.0)
73+
74+
terminal.write(repeating(string: "=", count: n) + repeating(string: "-", count: barWidth - n), inColor: .green)
75+
terminal.write("]", inColor: .green, bold: true)
76+
terminal.endLine()
77+
78+
terminal.clearLine()
79+
if text.utf8.count > width {
80+
let prefix = ""
81+
terminal.write(prefix)
82+
terminal.write(String(text.suffix(width - prefix.utf8.count)))
83+
} else {
84+
terminal.write(text)
85+
}
86+
}
87+
88+
func complete(success: Bool) {
89+
terminal.endLine()
90+
terminal.endLine()
91+
}
92+
93+
func clear() {
94+
terminal.clearLine()
95+
terminal.moveCursor(up: 1)
96+
terminal.clearLine()
97+
}
98+
}
99+
100+
/// A multi-line percent-based progress animation.
101+
final class MultiLinePercentProgressAnimation: ProgressAnimationProtocol {
102+
private struct Info: Equatable {
103+
let percentage: Int
104+
let text: String
105+
}
106+
107+
private let stream: WritableByteStream
108+
private let header: String
109+
private var hasDisplayedHeader = false
110+
private var lastDisplayedText: String? = nil
111+
112+
init(stream: WritableByteStream, header: String) {
113+
self.stream = stream
114+
self.header = header
115+
}
116+
117+
func update(step: Int, total: Int, text: String) {
118+
assert(step <= total)
119+
120+
if !hasDisplayedHeader, !header.isEmpty {
121+
stream.send(header)
122+
stream.send("\n")
123+
stream.flush()
124+
hasDisplayedHeader = true
125+
}
126+
127+
let percentage = step * 100 / total
128+
stream.send("\(percentage)%: ").send(text)
129+
stream.send("\n")
130+
stream.flush()
131+
lastDisplayedText = text
132+
}
133+
134+
func complete(success: Bool) {
135+
}
136+
137+
func clear() {
138+
}
139+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import class TSCBasic.TerminalController
14+
import class TSCBasic.LocalFileOutputByteStream
15+
import protocol TSCBasic.WritableByteStream
16+
import protocol TSCUtility.ProgressAnimationProtocol
17+
18+
@_spi(SwiftPMInternal)
19+
public typealias ProgressAnimationProtocol = TSCUtility.ProgressAnimationProtocol
20+
21+
/// Namespace to nest public progress animations under.
22+
@_spi(SwiftPMInternal)
23+
public enum ProgressAnimation {
24+
/// Dynamically create a progress animation based on the current stream
25+
/// capabilities and desired verbosity.
26+
///
27+
/// - Parameters:
28+
/// - stream: A stream to write animations into.
29+
/// - verbose: The verbosity level of other output in the system.
30+
/// - ttyTerminalAnimationFactory: A progress animation to use when the
31+
/// output stream is connected to a terminal with support for special
32+
/// escape sequences.
33+
/// - dumbTerminalAnimationFactory: A progress animation to use when the
34+
/// output stream is connected to a terminal without support for special
35+
/// escape sequences for clearing lines or controlling cursor positions.
36+
/// - defaultAnimationFactory: A progress animation to use when the
37+
/// desired output is verbose or the output stream verbose or is not
38+
/// connected to a terminal, e.g. a pipe or file.
39+
/// - Returns: A progress animation instance matching the stream
40+
/// capabilities and desired verbosity.
41+
static func dynamic(
42+
stream: WritableByteStream,
43+
verbose: Bool,
44+
ttyTerminalAnimationFactory: (TerminalController) -> any ProgressAnimationProtocol,
45+
dumbTerminalAnimationFactory: () -> any ProgressAnimationProtocol,
46+
defaultAnimationFactory: () -> any ProgressAnimationProtocol
47+
) -> any ProgressAnimationProtocol {
48+
if let terminal = TerminalController(stream: stream), !verbose {
49+
return ttyTerminalAnimationFactory(terminal)
50+
} else if let fileStream = stream as? LocalFileOutputByteStream,
51+
TerminalController.terminalType(fileStream) == .dumb
52+
{
53+
return dumbTerminalAnimationFactory()
54+
} else {
55+
return defaultAnimationFactory()
56+
}
57+
}
58+
}
59+
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import class TSCBasic.TerminalController
14+
import protocol TSCBasic.WritableByteStream
15+
16+
/// A single line percent-based progress animation.
17+
final class SingleLinePercentProgressAnimation: ProgressAnimationProtocol {
18+
private let stream: WritableByteStream
19+
private let header: String?
20+
private var displayedPercentages: Set<Int> = []
21+
private var hasDisplayedHeader = false
22+
23+
init(stream: WritableByteStream, header: String?) {
24+
self.stream = stream
25+
self.header = header
26+
}
27+
28+
func update(step: Int, total: Int, text: String) {
29+
if let header = header, !hasDisplayedHeader {
30+
stream.send(header)
31+
stream.send("\n")
32+
stream.flush()
33+
hasDisplayedHeader = true
34+
}
35+
36+
let percentage = step * 100 / total
37+
let roundedPercentage = Int(Double(percentage / 10).rounded(.down)) * 10
38+
if percentage != 100, !displayedPercentages.contains(roundedPercentage) {
39+
stream.send(String(roundedPercentage)).send(".. ")
40+
displayedPercentages.insert(roundedPercentage)
41+
}
42+
43+
stream.flush()
44+
}
45+
46+
func complete(success: Bool) {
47+
if success {
48+
stream.send("OK")
49+
stream.flush()
50+
}
51+
}
52+
53+
func clear() {
54+
}
55+
}

0 commit comments

Comments
 (0)