Skip to content

Commit d058b30

Browse files
committed
WIP: Blast progress bar
1 parent b198806 commit d058b30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2283
-508
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 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 Foundation
14+
15+
extension FormatStyle where Self == Duration.UnitsFormatStyle {
16+
static var blast: Self {
17+
.units(
18+
width: .narrow,
19+
fractionalPart: .init(lengthLimits: 0...2))
20+
}
21+
}
22+
23+
class BlastProgressAnimation {
24+
// Dependencies
25+
var terminal: BlastTerminalController
26+
27+
// Configuration
28+
var interactive: Bool
29+
var verbose: Bool
30+
var header: String?
31+
32+
// Internal state
33+
var mostRecentTask: String
34+
var drawnLines: Int
35+
var state: ProgressState
36+
37+
required init(
38+
stream: any WritableByteStream,
39+
coloring: TerminalColoring,
40+
interactive: Bool,
41+
verbose: Bool,
42+
header: String?
43+
) {
44+
self.terminal = BlastTerminalController(
45+
stream: stream,
46+
coloring: coloring)
47+
self.interactive = interactive
48+
self.verbose = verbose
49+
self.header = header
50+
self.mostRecentTask = ""
51+
self.drawnLines = 0
52+
self.state = .init()
53+
}
54+
}
55+
56+
extension BlastProgressAnimation: ProgressAnimationProtocol2 {
57+
func update(
58+
id: Int,
59+
name: String,
60+
event: ProgressTaskState,
61+
at time: ContinuousClock.Instant
62+
) {
63+
let update = self.state.update(
64+
id: id,
65+
name: name,
66+
state: event,
67+
at: time)
68+
guard let (task, state) = update else { return }
69+
70+
if self.interactive {
71+
self._clear()
72+
}
73+
74+
if self.verbose || true, case .completed(let duration) = state {
75+
self._draw(task: task, duration: duration)
76+
self.terminal.newLine()
77+
}
78+
79+
if self.interactive {
80+
self._draw()
81+
} else if case .started = state {
82+
// For the non-interactive case, only re-draw the status bar when a
83+
// new task starts
84+
self._drawStates()
85+
self.terminal.write(" ")
86+
self.terminal.write(task.name)
87+
self.terminal.newLine()
88+
}
89+
90+
self._flush()
91+
}
92+
93+
func draw() {
94+
guard self.interactive else { return }
95+
self._draw()
96+
self._flush()
97+
}
98+
99+
func complete() {
100+
self._complete()
101+
self._flush()
102+
}
103+
104+
func clear() {
105+
guard self.interactive else { return }
106+
self._clear()
107+
self._flush()
108+
}
109+
}
110+
111+
extension BlastProgressAnimation {
112+
func _draw(state: ProgressTaskState) {
113+
self.terminal.text(styles: .foregroundColor(state.visualColor), .bold)
114+
self.terminal.write(state.visualSymbol)
115+
}
116+
117+
func _draw(task: ProgressTask, duration: ContinuousClock.Duration?) {
118+
self.terminal.write(" ")
119+
self._draw(state: task.state)
120+
self.terminal.text(styles: .reset)
121+
self.terminal.write(" ")
122+
self.terminal.write(task.name)
123+
if let duration {
124+
self.terminal.text(styles: .foregroundColor(.white), .bold)
125+
self.terminal.write(" (\(duration.formatted(.blast)))")
126+
self.terminal.text(styles: .reset)
127+
}
128+
}
129+
130+
func _draw(state: ProgressTaskState, count: Int, last: Bool) {
131+
self.terminal.text(styles: .notItalicNorBold, .foregroundColor(state.visualColor))
132+
self.terminal.write(state.visualSymbol)
133+
self.terminal.write(" \(count)")
134+
self.terminal.text(styles: .defaultForegroundColor, .bold)
135+
if !last {
136+
self.terminal.write(", ")
137+
}
138+
}
139+
140+
func _drawStates() {
141+
self.terminal.text(styles: .bold)
142+
self.terminal.write("(")
143+
self._draw(state: .discovered, count: self.state.counts.pending, last: false)
144+
self._draw(state: .started, count: self.state.counts.running, last: false)
145+
self._draw(state: .completed(.succeeded), count: self.state.counts.succeeded, last: false)
146+
self._draw(state: .completed(.failed), count: self.state.counts.failed, last: false)
147+
self._draw(state: .completed(.cancelled), count: self.state.counts.cancelled, last: false)
148+
self._draw(state: .completed(.skipped), count: self.state.counts.skipped, last: true)
149+
self.terminal.write(")")
150+
self.terminal.text(styles: .reset)
151+
}
152+
153+
func _draw() {
154+
assert(self.drawnLines == 0)
155+
self._drawStates()
156+
if let header = self.header {
157+
self.terminal.write(" ")
158+
self.terminal.write(header)
159+
}
160+
self.drawnLines += 1
161+
let tasks = self.state.tasks.values.filter { $0.state == .started }.sorted()
162+
for task in tasks {
163+
self.terminal.newLine()
164+
self._draw(task: task, duration: nil)
165+
self.drawnLines += 1
166+
}
167+
}
168+
169+
func _complete() {
170+
self._clear()
171+
self._drawStates()
172+
self.terminal.newLine()
173+
}
174+
175+
func _clear() {
176+
guard self.drawnLines > 0 else { return }
177+
self.terminal.eraseLine(.entire)
178+
self.terminal.carriageReturn()
179+
for _ in 1..<self.drawnLines {
180+
self.terminal.moveCursorPrevious(lines: 1)
181+
self.terminal.eraseLine(.entire)
182+
}
183+
self.drawnLines = 0
184+
}
185+
186+
func _flush() {
187+
self.terminal.flush()
188+
}
189+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 protocol TSCBasic.WritableByteStream
14+
15+
/// A multi-line ninja-like progress animation.
16+
final class NinjaMultiLineProgressAnimation {
17+
// Dependencies
18+
var terminal: BlastTerminalController
19+
20+
// Internal state
21+
var text: String
22+
var state: ProgressState
23+
24+
required init(
25+
stream: any WritableByteStream,
26+
coloring: TerminalColoring,
27+
interactive: Bool,
28+
verbose: Bool,
29+
header: String?
30+
) {
31+
self.terminal = BlastTerminalController(
32+
stream: stream,
33+
coloring: coloring)
34+
self.text = ""
35+
self.state = .init()
36+
}
37+
}
38+
39+
extension NinjaMultiLineProgressAnimation: ProgressAnimationProtocol2 {
40+
41+
42+
func update(
43+
id: Int,
44+
name: String,
45+
event: ProgressTaskState,
46+
at time: ContinuousClock.Instant
47+
) {
48+
let update = self.state.update(
49+
id: id,
50+
name: name,
51+
state: event,
52+
at: time)
53+
guard let (task, _) = update else { return }
54+
guard self.text != task.name else { return }
55+
self.text = task.name
56+
self.terminal.write(
57+
"[\(self.state.counts.completed)/\(self.state.counts.total)] ")
58+
self.terminal.write(self.text)
59+
self.terminal.newLine()
60+
self.terminal.flush()
61+
}
62+
63+
func draw() {}
64+
65+
func complete() {}
66+
67+
func clear() {}
68+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
/// A redrawing ninja-like progress animation.
14+
final class NinjaRedrawingProgressAnimation {
15+
// Dependencies
16+
var terminal: BlastTerminalController
17+
18+
// Internal state
19+
var text: String
20+
var hasDisplayedProgress: Bool
21+
var state: ProgressState
22+
23+
required init(
24+
stream: any WritableByteStream,
25+
coloring: TerminalColoring,
26+
interactive: Bool,
27+
verbose: Bool,
28+
header: String?
29+
) {
30+
self.terminal = BlastTerminalController(
31+
stream: stream,
32+
coloring: coloring)
33+
self.text = ""
34+
self.hasDisplayedProgress = false
35+
self.state = .init()
36+
}
37+
}
38+
39+
extension NinjaRedrawingProgressAnimation: ProgressAnimationProtocol2 {
40+
func update(
41+
id: Int,
42+
name: String,
43+
event: ProgressTaskState,
44+
at time: ContinuousClock.Instant
45+
) {
46+
let update = self.state.update(
47+
id: id,
48+
name: name,
49+
state: event,
50+
at: time)
51+
guard let (task, _) = update else { return }
52+
self.text = task.name
53+
54+
self._clear()
55+
self._draw()
56+
self._flush()
57+
}
58+
59+
func draw() {
60+
self._draw()
61+
self._flush()
62+
}
63+
64+
func complete() {
65+
self._complete()
66+
self._flush()
67+
}
68+
69+
func clear() {
70+
self._clear()
71+
self._flush()
72+
}
73+
}
74+
75+
extension NinjaRedrawingProgressAnimation {
76+
func _draw() {
77+
assert(!self.hasDisplayedProgress)
78+
let progressText = "[\(self.state.counts.completed)/\(self.state.counts.total)] \(self.text)"
79+
// FIXME: self.terminal.width
80+
let width = 80
81+
if progressText.utf8.count > width {
82+
let suffix = ""
83+
self.terminal.write(String(progressText.prefix(width - suffix.utf8.count)))
84+
self.terminal.write(suffix)
85+
} else {
86+
self.terminal.write(progressText)
87+
}
88+
self.hasDisplayedProgress = true
89+
}
90+
91+
func _complete() {
92+
if self.hasDisplayedProgress {
93+
self.terminal.newLine()
94+
}
95+
}
96+
97+
func _clear() {
98+
guard self.hasDisplayedProgress else { return }
99+
self.terminal.eraseLine(.entire)
100+
self.terminal.carriageReturn()
101+
self.hasDisplayedProgress = false
102+
}
103+
104+
func _flush() {
105+
self.terminal.flush()
106+
}
107+
}

0 commit comments

Comments
 (0)