Skip to content

Commit b671690

Browse files
authored
ProgressBar: Various fixes (apple#1025)
There's a couple things I don't think are intuitive about this. 1. Because of the internal task, render() can still be called even after finish() completes. Ideally async defers are supported and we could just await the final render completing after cancelling the task and setting .finished, but alas. To fix this we can just lock across the methods for now. 2. We always clear the screen in the destructor, even if we don't use the progress bar. I don't think we should honestly do anything in the destructor. Feels a programmer error not to defer { bar.finish() } or call it somewhere. 3. Our spaces based line clearing. Use the ansi escape sequence for clearing line; I think our calculations were slightly off and it would leave trailing output ( "s]" ) in some cases. 4. Shrinking the window until the output is smaller than the terminal window (and vice versa) is wonky on various term emulators. Truthfully, this is just a hard problem, but we can truncate our output and still provide some useful info. This fixes some single line output (cat /etc/hostname etc.) getting cleared in our atexit handler, as well as the need for the usleep.
1 parent 98410fd commit b671690

File tree

3 files changed

+198
-118
lines changed

3 files changed

+198
-118
lines changed

Sources/TerminalProgress/ProgressBar+State.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
import Foundation
1818

1919
extension ProgressBar {
20-
/// A configuration struct for the progress bar.
21-
public struct State {
20+
/// State for the progress bar.
21+
struct State {
2222
/// A flag indicating whether the progress bar is finished.
23-
public var finished = false
23+
var finished = false
2424
var iteration = 0
2525
private let speedInterval: DispatchTimeInterval = .seconds(1)
2626

@@ -41,6 +41,7 @@ extension ProgressBar {
4141
calculateSizeSpeed()
4242
}
4343
}
44+
4445
var totalSize: Int64?
4546
private var sizeUpdateSpeed: String?
4647
var sizeSpeed: String? {
@@ -66,6 +67,7 @@ extension ProgressBar {
6667

6768
var startTime: DispatchTime
6869
var output = ""
70+
var renderTask: Task<Void, Never>?
6971

7072
init(
7173
description: String = "", subDescription: String = "", itemsName: String = "", tasks: Int = 0, totalTasks: Int? = nil, items: Int = 0, totalItems: Int? = nil,

Sources/TerminalProgress/ProgressBar+Terminal.swift

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,39 @@ enum EscapeSequence {
2121
static let hideCursor = "\u{001B}[?25l"
2222
static let showCursor = "\u{001B}[?25h"
2323
static let moveUp = "\u{001B}[1A"
24+
static let clearToEndOfLine = "\u{001B}[K"
2425
}
2526

2627
extension ProgressBar {
27-
private var terminalWidth: Int {
28+
var termWidth: Int {
2829
guard
2930
let terminalHandle = term,
3031
let terminal = try? Terminal(descriptor: terminalHandle.fileDescriptor)
3132
else {
3233
return 0
3334
}
3435

35-
let terminalWidth = (try? Int(terminal.size.width)) ?? 0
36-
return terminalWidth
36+
return (try? Int(terminal.size.width)) ?? 0
3737
}
3838

3939
/// Clears the progress bar and resets the cursor.
4040
public func clearAndResetCursor() {
41-
clear()
42-
resetCursor()
41+
state.withLock { s in
42+
clear(state: &s)
43+
resetCursor()
44+
}
4345
}
4446

4547
/// Clears the progress bar.
4648
public func clear() {
47-
displayText("")
49+
state.withLock { s in
50+
clear(state: &s)
51+
}
52+
}
53+
54+
/// Clears the progress bar (caller must hold state lock).
55+
func clear(state: inout State) {
56+
displayText("", state: &state)
4857
}
4958

5059
/// Resets the cursor.
@@ -63,27 +72,24 @@ extension ProgressBar {
6372
}
6473

6574
func displayText(_ text: String, terminating: String = "\r") {
66-
var text = text
67-
68-
// Clears previously printed characters if the new string is shorter.
69-
printedWidth.withLock {
70-
text += String(repeating: " ", count: max($0 - text.count, 0))
71-
$0 = text.count
72-
}
73-
state.withLock {
74-
$0.output = text
75+
state.withLock { s in
76+
displayText(text, state: &s, terminating: terminating)
7577
}
78+
}
79+
80+
func displayText(_ text: String, state: inout State, terminating: String = "\r") {
81+
state.output = text
7682

7783
// Clears previously printed lines.
7884
var lines = ""
79-
if terminating.hasSuffix("\r") && terminalWidth > 0 {
80-
let lineCount = (text.count - 1) / terminalWidth
85+
if terminating.hasSuffix("\r") && termWidth > 0 {
86+
let lineCount = (text.count - 1) / termWidth
8187
for _ in 0..<lineCount {
8288
lines += EscapeSequence.moveUp
8389
}
8490
}
8591

86-
text = "\(text)\(terminating)\(lines)"
87-
display(text)
92+
let output = "\(text)\(EscapeSequence.clearToEndOfLine)\(terminating)\(lines)"
93+
display(output)
8894
}
8995
}

0 commit comments

Comments
 (0)