Skip to content
5 changes: 4 additions & 1 deletion Sources/CLI/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ struct Application: AsyncParsableCommand {
signal(SIGTERM, signalHandler)
// Normal and explicit exit.
atexit {
ProgressBar.resetCursor()
if let progressConfig = try? ProgressConfig() {
let progressBar = ProgressBar(config: progressConfig)
progressBar.resetCursor()
}
}
}

Expand Down
29 changes: 11 additions & 18 deletions Sources/TerminalProgress/ProgressBar+Terminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ enum EscapeSequence {
}

extension ProgressBar {
static var terminalWidth: Int {
private var terminalWidth: Int {
guard
let termimalHandle = ProgressBar.term,
let termimalHandle = term,
let terminal = try? Terminal(descriptor: termimalHandle.fileDescriptor)
else {
return 0
Expand All @@ -39,28 +39,20 @@ extension ProgressBar {
/// Clears the progress bar and resets the cursor.
public func clearAndResetCursor() {
clear()
ProgressBar.resetCursor()
resetCursor()
}

/// Clears the progress bar.
public func clear() {
// We can't use "\u{001B}[2K" for clearing the line because this may lead to a race with `stdout` when using `stderr` for progress updates.
displayText("")
}

/// Resets the cursor.
static public func resetCursor() {
ProgressBar.display(EscapeSequence.showCursor)
public func resetCursor() {
display(EscapeSequence.showCursor)
}

static func getTerminal() -> FileHandle? {
let standardError = FileHandle.standardError
let fd = standardError.fileDescriptor
let isATTY = isatty(fd)
return isATTY == 1 ? standardError : nil
}

static func display(_ text: String) {
func display(_ text: String) {
guard let term else {
return
}
Expand All @@ -74,19 +66,20 @@ extension ProgressBar {
var text = text

// Clears previously printed characters if the new string is shorter.
text += String(repeating: " ", count: max(state.output.count - text.count, 0))
text += String(repeating: " ", count: max(printedWidth - text.count, 0))
printedWidth = text.count
state.output = text

// Clears previously printed lines.
var lines = ""
if ProgressBar.terminalWidth > 0 {
let lineCount = (text.count - 1) / ProgressBar.terminalWidth
if terminating.hasSuffix("\r") && terminalWidth > 0 {
let lineCount = (text.count - 1) / terminalWidth
for _ in 0..<lineCount {
lines += EscapeSequence.moveUp
}
}

text = "\(text)\(terminating)\(lines)"
ProgressBar.display(text)
display(text)
}
}
30 changes: 17 additions & 13 deletions Sources/TerminalProgress/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,27 @@ public final class ProgressBar: Sendable {
let config: ProgressConfig
@SendableProperty
var state: State
static let term: FileHandle? = getTerminal()
static let termQueue = DispatchQueue(label: "com.apple.container.ProgressBar")
@SendableProperty
var printedWidth = 0
let term: FileHandle?
let termQueue = DispatchQueue(label: "com.apple.container.ProgressBar")
private let standardError = StandardError()

/// Returns `true` if the progress bar has finished.
public var isFinished: Bool {
state.finished
}

/// Creates a new progress bar.
/// - Parameter config: The configuration for the progress bar.
public init(config: ProgressConfig) {
self.config = config
term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil
state = State(
description: config.initialDescription, itemsName: config.initialItemsName, totalTasks: config.initialTotalTasks,
totalItems: config.initialTotalItems,
totalSize: config.initialTotalSize)
ProgressBar.display(EscapeSequence.hideCursor)
display(EscapeSequence.hideCursor)
}

deinit {
Expand All @@ -48,15 +56,9 @@ public final class ProgressBar: Sendable {

/// Allows resetting the progress state of the current task.
public func resetCurrentTask() {
clear()
state = State(description: state.description, itemsName: state.itemsName, tasks: state.tasks, totalTasks: state.totalTasks, startTime: state.startTime)
}

/// Returns `true` if the progress bar has finished.
public func isFinished() -> Bool {
state.finished
}

private func printFullDescription() {
if state.subDescription != "" {
standardError.write("\(state.description) \(state.subDescription)")
Expand Down Expand Up @@ -95,7 +97,7 @@ public final class ProgressBar: Sendable {
printFullDescription()
}

while !self.isFinished() {
while !isFinished {
let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000)
render()
state.iteration += 1
Expand All @@ -115,7 +117,7 @@ public final class ProgressBar: Sendable {

/// Finishes the progress bar.
public func finish() {
guard !self.isFinished() else {
guard !isFinished else {
return
}

Expand All @@ -127,8 +129,10 @@ public final class ProgressBar: Sendable {
if config.clearOnFinish {
clearAndResetCursor()
} else {
ProgressBar.resetCursor()
resetCursor()
}
// Allow printed output to flush.
usleep(100_000)
}
}

Expand All @@ -140,7 +144,7 @@ extension ProgressBar {
}

func render() {
guard ProgressBar.term != nil && !config.disableProgressUpdates else {
guard term != nil && !config.disableProgressUpdates && !isFinished else {
return
}
let output = draw()
Expand Down
5 changes: 5 additions & 0 deletions Sources/TerminalProgress/ProgressConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import Foundation

/// A configuration for displaying a progress bar.
public struct ProgressConfig: Sendable {
/// The file handle for progress updates.
let terminal: FileHandle
/// The initial description of the progress bar.
let initialDescription: String
/// The initial additional description of the progress bar.
Expand Down Expand Up @@ -65,6 +67,7 @@ public struct ProgressConfig: Sendable {
public let disableProgressUpdates: Bool
/// Creates a new instance of `ProgressConfig`.
/// - Parameters:
/// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`.
/// - description: The initial description of the progress bar. The default value is `""`.
/// - subDescription: The initial additional description of the progress bar. The default value is `""`.
/// - itemsName: The initial items name. The default value is `"it"`.
Expand All @@ -86,6 +89,7 @@ public struct ProgressConfig: Sendable {
/// - clearOnFinish: The flag indicating whether to clear the progress bar before reseting the cursor. The default is `true`.
/// - disableProgressUpdates: The flag indicating whether to update the progress bar. The default is `false`.
public init(
terminal: FileHandle = .standardError,
description: String = "",
subDescription: String = "",
itemsName: String = "it",
Expand Down Expand Up @@ -123,6 +127,7 @@ public struct ProgressConfig: Sendable {
}
}

self.terminal = terminal
self.initialDescription = description
self.initialSubDescription = subDescription
self.initialItemsName = itemsName
Expand Down