Skip to content
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] {
scDependency = .package(path: path)
scVersion = "latest"
} else {
scVersion = "0.1.0"
scVersion = "0.1.1"
scDependency = .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion))
}

Expand Down
1 change: 0 additions & 1 deletion Sources/ContainerXPC/XPCClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import ContainerizationError
import Foundation

public struct XPCClient: Sendable {
// Access to `connection` is protected by a lock
private nonisolated(unsafe) let connection: xpc_connection_t
private let q: DispatchQueue?
private let service: String
Expand Down
99 changes: 65 additions & 34 deletions Sources/TerminalProgress/ProgressBar+Add.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,24 @@ extension ProgressBar {

/// Performs a check to see if the progress bar should be finished.
public func checkIfFinished() {
if let totalTasks = state.totalTasks {
var finished = true
var defined = false
if let totalTasks = state.totalTasks, totalTasks > 0 {
// For tasks, we're showing the current task rather then the number of completed tasks.
guard state.tasks > totalTasks else {
return
}
finished = finished && state.tasks == totalTasks
defined = true
}
if let totalItems = state.totalItems {
guard state.items == totalItems else {
return
}
if let totalItems = state.totalItems, totalItems > 0 {
finished = finished && state.items == totalItems
defined = true
}
if let totalSize = state.totalSize {
guard state.size == totalSize else {
return
}
if let totalSize = state.totalSize, totalSize > 0 {
finished = finished && state.size == totalSize
defined = true
}
if defined && finished {
finish()
}
finish()
}

/// Sets the current tasks.
Expand All @@ -92,9 +93,14 @@ extension ProgressBar {

/// Performs an addition to the current tasks.
/// - Parameter tasks: The tasks to add to the current tasks.
public func add(tasks toAdd: Int, render: Bool = true) {
let newTasks = state.tasks + toAdd
set(tasks: newTasks, render: render)
public func add(tasks delta: Int, render: Bool = true) {
_state.withLock {
let newTasks = $0.tasks + delta
$0.tasks = newTasks
}
if render {
self.render()
}
}

/// Sets the total tasks.
Expand All @@ -108,10 +114,15 @@ extension ProgressBar {

/// Performs an addition to the total tasks.
/// - Parameter totalTasks: The tasks to add to the total tasks.
public func add(totalTasks toAdd: Int, render: Bool = true) {
let totalTasks = state.totalTasks ?? 0
let newTotalTasks = totalTasks + toAdd
set(totalTasks: newTotalTasks, render: render)
public func add(totalTasks delta: Int, render: Bool = true) {
_state.withLock {
let totalTasks = $0.totalTasks ?? 0
let newTotalTasks = totalTasks + delta
$0.totalTasks = newTotalTasks
}
if render {
self.render()
}
}

/// Sets the items name.
Expand All @@ -134,9 +145,14 @@ extension ProgressBar {

/// Performs an addition to the current items.
/// - Parameter items: The items to add to the current items.
public func add(items toAdd: Int, render: Bool = true) {
let newItems = state.items + toAdd
set(items: newItems, render: render)
public func add(items delta: Int, render: Bool = true) {
_state.withLock {
let newItems = $0.items + delta
$0.items = newItems
}
if render {
self.render()
}
}

/// Sets the total items.
Expand All @@ -150,10 +166,15 @@ extension ProgressBar {

/// Performs an addition to the total items.
/// - Parameter totalItems: The items to add to the total items.
public func add(totalItems toAdd: Int, render: Bool = true) {
let totalItems = state.totalItems ?? 0
let newTotalItems = totalItems + toAdd
set(totalItems: newTotalItems, render: render)
public func add(totalItems delta: Int, render: Bool = true) {
_state.withLock {
let totalItems = $0.totalItems ?? 0
let newTotalItems = totalItems + delta
$0.totalItems = newTotalItems
}
if render {
self.render()
}
}

/// Sets the current size.
Expand All @@ -167,9 +188,14 @@ extension ProgressBar {

/// Performs an addition to the current size.
/// - Parameter size: The size to add to the current size.
public func add(size toAdd: Int64, render: Bool = true) {
let newSize = state.size + toAdd
set(size: newSize, render: render)
public func add(size delta: Int64, render: Bool = true) {
_state.withLock {
let newSize = $0.size + delta
$0.size = newSize
}
if render {
self.render()
}
}

/// Sets the total size.
Expand All @@ -183,9 +209,14 @@ extension ProgressBar {

/// Performs an addition to the total size.
/// - Parameter totalSize: The size to add to the total size.
public func add(totalSize toAdd: Int64, render: Bool = true) {
let totalSize = state.totalSize ?? 0
let newTotalSize = totalSize + toAdd
set(totalSize: newTotalSize, render: render)
public func add(totalSize delta: Int64, render: Bool = true) {
_state.withLock {
let totalSize = $0.totalSize ?? 0
let newTotalSize = totalSize + delta
$0.totalSize = newTotalSize
}
if render {
self.render()
}
}
}
95 changes: 60 additions & 35 deletions Sources/TerminalProgress/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import SendableProperty
/// 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()`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this property should up in IDEs? Would the user be able to navigate into the def for the property?

Copy link
Contributor Author

@dkovba dkovba Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It it possible to see the definition of the _state property by expanding the macro using the corresponding command in the context menu:

Screenshot 2025-06-12 at 4 29 54 PM

@SendableProperty
var state: State
var state = State()
@SendableProperty
var printedWidth = 0
let term: FileHandle?
Expand Down Expand Up @@ -97,7 +98,7 @@ public final class ProgressBar: Sendable {
printFullDescription()
}

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

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

state.finished = true

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

if !config.disableProgressUpdates && !config.clearOnFinish {
displayText(state.output, terminating: "\n")
}
Expand All @@ -143,8 +148,8 @@ extension ProgressBar {
return timeDifferenceSeconds
}

func render() {
guard term != nil && !config.disableProgressUpdates && !isFinished else {
func render(force: Bool = false) {
guard term != nil && !config.disableProgressUpdates && (force || !state.finished) else {
return
}
let output = draw()
Expand All @@ -154,8 +159,12 @@ extension ProgressBar {
func draw() -> String {
var components = [String]()
if config.showSpinner && !config.showProgressBar {
let spinnerIcon = config.theme.getSpinnerIcon(state.iteration)
components.append("\(spinnerIcon)")
if !state.finished {
let spinnerIcon = config.theme.getSpinnerIcon(state.iteration)
components.append("\(spinnerIcon)")
} else {
components.append("\(config.theme.done)")
}
}

if config.showTasks, let totalTasks = state.totalTasks {
Expand All @@ -176,13 +185,13 @@ extension ProgressBar {
let total = state.totalSize ?? Int64(state.totalItems ?? 0)

if config.showPercent && total > 0 && allowProgress {
components.append("\(state.percent)")
components.append("\(state.finished ? "100%" : state.percent)")
}

if config.showProgressBar, total > 0, allowProgress {
let usedWidth = components.joined(separator: " ").count + 45 /* the maximum number of characters we may need */
let remainingWidth = max(config.width - usedWidth, 1 /* the minumum width of a progress bar */)
let barLength = Int(Int64(remainingWidth) * value / total)
let barLength = state.finished ? remainingWidth : Int(Int64(remainingWidth) * value / total)
let barPaddingLength = remainingWidth - barLength
let bar = "\(String(repeating: config.theme.bar, count: barLength))\(String(repeating: " ", count: barPaddingLength))"
components.append("|\(bar)|")
Expand All @@ -195,40 +204,56 @@ extension ProgressBar {
if !state.itemsName.isEmpty {
itemsName = " \(state.itemsName)"
}
if let totalItems = state.totalItems {
additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)")
if state.finished {
if let totalItems = state.totalItems {
additionalComponents.append("\(totalItems.formattedNumber())\(itemsName)")
}
} else {
additionalComponents.append("\(state.items.formattedNumber())\(itemsName)")
if let totalItems = state.totalItems {
additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)")
} else {
additionalComponents.append("\(state.items.formattedNumber())\(itemsName)")
}
}
}

if state.size > 0 && allowProgress {
var formattedCombinedSize = ""
if config.showSize {
var formattedSize = state.size.formattedSize()
formattedSize = adjustFormattedSize(formattedSize)
if let totalSize = state.totalSize {
var formattedTotalSize = totalSize.formattedSize()
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize)
} else {
formattedCombinedSize = formattedSize
if state.finished {
if config.showSize {
if let totalSize = state.totalSize {
var formattedTotalSize = totalSize.formattedSize()
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
additionalComponents.append(formattedTotalSize)
}
}
} else {
var formattedCombinedSize = ""
if config.showSize {
var formattedSize = state.size.formattedSize()
formattedSize = adjustFormattedSize(formattedSize)
if let totalSize = state.totalSize {
var formattedTotalSize = totalSize.formattedSize()
formattedTotalSize = adjustFormattedSize(formattedTotalSize)
formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize)
} else {
formattedCombinedSize = formattedSize
}
}
}

var formattedSpeed = ""
if config.showSpeed {
formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)"
formattedSpeed = adjustFormattedSize(formattedSpeed)
}
var formattedSpeed = ""
if config.showSpeed {
formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)"
formattedSpeed = adjustFormattedSize(formattedSpeed)
}

if config.showSize && config.showSpeed {
additionalComponents.append(formattedCombinedSize)
additionalComponents.append(formattedSpeed)
} else if config.showSize {
additionalComponents.append(formattedCombinedSize)
} else if config.showSpeed {
additionalComponents.append(formattedSpeed)
if config.showSize && config.showSpeed {
additionalComponents.append(formattedCombinedSize)
additionalComponents.append(formattedSpeed)
} else if config.showSize {
additionalComponents.append(formattedCombinedSize)
} else if config.showSpeed {
additionalComponents.append(formattedSpeed)
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/TerminalProgress/ProgressTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
public protocol ProgressTheme: Sendable {
/// The icons used to represent a spinner.
var spinner: [String] { get }
/// The icons used to represent a progress bar.
/// The icon used to represent a progress bar.
var bar: String { get }
/// The icon used to indicate that a progress bar finished.
var done: String { get }
}

public struct DefaultProgressTheme: ProgressTheme {
public let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
public let bar = "█"
public let done = "✔"
}

extension ProgressTheme {
Expand Down
Loading