Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions Sources/TerminalProgress/ProgressBar+Add.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ extension ProgressBar {

/// Performs a check to see if the progress bar should be finished.
public func checkIfFinished() {
let state = self.state.withLock { $0 }

var finished = true
var defined = false
if let totalTasks = state.totalTasks, totalTasks > 0 {
Expand All @@ -85,7 +87,7 @@ extension ProgressBar {
/// - Parameter newTasks: The current tasks to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(tasks newTasks: Int, render: Bool = true) {
state.tasks = newTasks
state.withLock { $0.tasks = newTasks }
if render {
self.render()
}
Expand All @@ -96,7 +98,7 @@ extension ProgressBar {
/// - Parameter delta: The tasks to add to the current tasks.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(tasks delta: Int, render: Bool = true) {
_state.withLock {
state.withLock {
let newTasks = $0.tasks + delta
$0.tasks = newTasks
}
Expand All @@ -109,7 +111,7 @@ extension ProgressBar {
/// - Parameter newTotalTasks: The total tasks to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(totalTasks newTotalTasks: Int, render: Bool = true) {
state.totalTasks = newTotalTasks
state.withLock { $0.totalTasks = newTotalTasks }
if render {
self.render()
}
Expand All @@ -119,7 +121,7 @@ extension ProgressBar {
/// - Parameter delta: The tasks to add to the total tasks.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(totalTasks delta: Int, render: Bool = true) {
_state.withLock {
state.withLock {
let totalTasks = $0.totalTasks ?? 0
let newTotalTasks = totalTasks + delta
$0.totalTasks = newTotalTasks
Expand All @@ -133,7 +135,7 @@ extension ProgressBar {
/// - Parameter newItemsName: The current items to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(itemsName newItemsName: String, render: Bool = true) {
state.itemsName = newItemsName
state.withLock { $0.itemsName = newItemsName }
if render {
self.render()
}
Expand All @@ -142,7 +144,7 @@ extension ProgressBar {
/// Sets the current items.
/// - Parameter newItems: The current items to set.
public func set(items newItems: Int, render: Bool = true) {
state.items = newItems
state.withLock { $0.items = newItems }
if render {
self.render()
}
Expand All @@ -152,7 +154,7 @@ extension ProgressBar {
/// - Parameter delta: The items to add to the current items.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(items delta: Int, render: Bool = true) {
_state.withLock {
state.withLock {
let newItems = $0.items + delta
$0.items = newItems
}
Expand All @@ -165,7 +167,7 @@ extension ProgressBar {
/// - Parameter newTotalItems: The total items to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(totalItems newTotalItems: Int, render: Bool = true) {
state.totalItems = newTotalItems
state.withLock { $0.totalItems = newTotalItems }
if render {
self.render()
}
Expand All @@ -175,7 +177,7 @@ extension ProgressBar {
/// - Parameter delta: The items to add to the total items.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(totalItems delta: Int, render: Bool = true) {
_state.withLock {
state.withLock {
let totalItems = $0.totalItems ?? 0
let newTotalItems = totalItems + delta
$0.totalItems = newTotalItems
Expand All @@ -189,7 +191,7 @@ extension ProgressBar {
/// - Parameter newSize: The current size to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(size newSize: Int64, render: Bool = true) {
state.size = newSize
state.withLock { $0.size = newSize }
if render {
self.render()
}
Expand All @@ -199,7 +201,7 @@ extension ProgressBar {
/// - Parameter delta: The size to add to the current size.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(size delta: Int64, render: Bool = true) {
_state.withLock {
state.withLock {
let newSize = $0.size + delta
$0.size = newSize
}
Expand All @@ -212,7 +214,7 @@ extension ProgressBar {
/// - Parameter newTotalSize: The total size to set.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func set(totalSize newTotalSize: Int64, render: Bool = true) {
state.totalSize = newTotalSize
state.withLock { $0.totalSize = newTotalSize }
if render {
self.render()
}
Expand All @@ -222,7 +224,7 @@ extension ProgressBar {
/// - Parameter delta: The size to add to the total size.
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
public func add(totalSize delta: Int64, render: Bool = true) {
_state.withLock {
state.withLock {
let totalSize = $0.totalSize ?? 0
let newTotalSize = totalSize + delta
$0.totalSize = newTotalSize
Expand Down
4 changes: 3 additions & 1 deletion Sources/TerminalProgress/ProgressBar+Terminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ extension ProgressBar {
// Clears previously printed characters if the new string is shorter.
text += String(repeating: " ", count: max(printedWidth - text.count, 0))
printedWidth = text.count
state.output = text
state.withLock {
$0.output = text
}

// Clears previously printed lines.
var lines = ""
Expand Down
55 changes: 32 additions & 23 deletions Sources/TerminalProgress/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@

import Foundation
import SendableProperty
import Synchronization

/// A progress bar that updates itself as tasks are completed.
public final class ProgressBar: Sendable {
let config: ProgressConfig
// `@SendableProperty` adds `_state: Synchronized<State>`, which can be updated inside a lock using `_state.withLock()`.
@SendableProperty
var state = State()
let state: Mutex<State>
@SendableProperty
var printedWidth = 0
let term: FileHandle?
Expand All @@ -31,18 +30,19 @@ public final class ProgressBar: Sendable {

/// Returns `true` if the progress bar has finished.
public var isFinished: Bool {
state.finished
state.withLock { $0.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(
let state = State(
description: config.initialDescription, itemsName: config.initialItemsName, totalTasks: config.initialTotalTasks,
totalItems: config.initialTotalItems,
totalSize: config.initialTotalSize)
self.state = Mutex(state)
display(EscapeSequence.hideCursor)
}

Expand All @@ -52,19 +52,25 @@ public final class ProgressBar: Sendable {

/// Allows resetting the progress state.
public func reset() {
state = State(description: config.initialDescription)
state.withLock {
$0 = State(description: config.initialDescription)
}
}

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

private func printFullDescription() {
if state.subDescription != "" {
standardError.write("\(state.description) \(state.subDescription)")
let (description, subDescription) = state.withLock { ($0.description, $0.subDescription) }

if subDescription != "" {
standardError.write("\(description) \(subDescription)")
} else {
standardError.write(state.description)
standardError.write(description)
}
}

Expand All @@ -73,35 +79,36 @@ public final class ProgressBar: Sendable {
public func set(description: String) {
resetCurrentTask()

state.description = description
state.subDescription = ""
state.withLock {
$0.description = description
$0.subDescription = ""
$0.tasks += 1
}
if config.disableProgressUpdates {
printFullDescription()
}

state.tasks += 1
}

/// Updates the additional description of the progress bar.
/// - Parameter subDescription: The additional description of the action being performed.
public func set(subDescription: String) {
resetCurrentTask()

state.subDescription = subDescription
state.withLock { $0.subDescription = subDescription }
if config.disableProgressUpdates {
printFullDescription()
}
}

private func start(intervalSeconds: TimeInterval) async {
if config.disableProgressUpdates && !state.description.isEmpty {
if config.disableProgressUpdates && !state.withLock({ $0.description.isEmpty }) {
printFullDescription()
}

while !state.finished {
while !state.withLock({ $0.finished }) {
let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000)
render()
state.iteration += 1
state.withLock { $0.iteration += 1 }
if (try? await Task.sleep(nanoseconds: intervalNanoseconds)) == nil {
return
}
Expand All @@ -118,17 +125,17 @@ public final class ProgressBar: Sendable {

/// Finishes the progress bar.
public func finish() {
guard !state.finished else {
guard !state.withLock({ $0.finished }) else {
return
}

state.finished = true
state.withLock { $0.finished = true }

// The last render.
render(force: true)

if !config.disableProgressUpdates && !config.clearOnFinish {
displayText(state.output, terminating: "\n")
displayText(state.withLock { $0.output }, terminating: "\n")
}

if config.clearOnFinish {
Expand All @@ -143,20 +150,22 @@ public final class ProgressBar: Sendable {

extension ProgressBar {
private func secondsSinceStart() -> Int {
let timeDifferenceNanoseconds = DispatchTime.now().uptimeNanoseconds - state.startTime.uptimeNanoseconds
let timeDifferenceNanoseconds = DispatchTime.now().uptimeNanoseconds - state.withLock { $0.startTime.uptimeNanoseconds }
let timeDifferenceSeconds = Int(floor(Double(timeDifferenceNanoseconds) / 1_000_000_000))
return timeDifferenceSeconds
}

func render(force: Bool = false) {
guard term != nil && !config.disableProgressUpdates && (force || !state.finished) else {
guard term != nil && !config.disableProgressUpdates && (force || !state.withLock { $0.finished }) else {
return
}
let output = draw()
displayText(output)
}

func draw() -> String {
let state = self.state.withLock { $0 }

var components = [String]()
if config.showSpinner && !config.showProgressBar {
if !state.finished {
Expand Down