Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
17 changes: 6 additions & 11 deletions Sources/ContainerCommands/Container/ContainerRun.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,12 @@ extension Application {
var exitCode: Int32 = 127
let id = Utility.createContainerID(name: self.managementFlags.name)

var progressConfig: ProgressConfig
switch self.progressFlags.progress {
case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true)
case .ansi:
progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
ignoreSmallSize: true,
totalTasks: 6
)
}
let progressConfig = try self.progressFlags.makeConfig(
showTasks: true,
showItems: true,
ignoreSmallSize: true,
totalTasks: 6
)

let progress = ProgressBar(config: progressConfig)
defer {
Expand Down
54 changes: 54 additions & 0 deletions Sources/ContainerCommands/Flags+ProgressConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerAPIClient
import TerminalProgress

extension Flags.Progress {
/// Creates a `ProgressConfig` based on the selected progress type.
///
/// For `.none`, progress updates are disabled. For `.ansi`, the given parameters
/// are used as-is. For `.plain`, ANSI-incompatible features (spinner, clear on finish)
/// are disabled and the output mode is set to `.plain`.
func makeConfig(
description: String = "",
itemsName: String = "it",
showTasks: Bool = false,
showItems: Bool = false,
showSpeed: Bool = true,
ignoreSmallSize: Bool = false,
totalTasks: Int? = nil
) throws -> ProgressConfig {
switch progress {
case .none:
return try ProgressConfig(disableProgressUpdates: true)
case .ansi, .plain:
let isPlain = progress == .plain
return try ProgressConfig(
description: description,
itemsName: itemsName,
showSpinner: !isPlain,
showTasks: showTasks,
showItems: showItems,
showSpeed: showSpeed,
ignoreSmallSize: ignoreSmallSize,
totalTasks: totalTasks,
clearOnFinish: !isPlain,
outputMode: isPlain ? .plain : .ansi
)
}
}
}
17 changes: 6 additions & 11 deletions Sources/ContainerCommands/Image/ImagePull.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,12 @@ extension Application {

let processedReference = try ClientImage.normalizeReference(reference)

var progressConfig: ProgressConfig
switch self.progressFlags.progress {
case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true)
case .ansi:
progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
ignoreSmallSize: true,
totalTasks: 2
)
}
let progressConfig = try self.progressFlags.makeConfig(
showTasks: true,
showItems: true,
ignoreSmallSize: true,
totalTasks: 2
)

let progress = ProgressBar(config: progressConfig)
defer {
Expand Down
19 changes: 7 additions & 12 deletions Sources/ContainerCommands/Image/ImagePush.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,13 @@ extension Application {
let scheme = try RequestScheme(registry.scheme)
let image = try await ClientImage.get(reference: reference)

var progressConfig: ProgressConfig
switch self.progressFlags.progress {
case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true)
case .ansi:
progressConfig = try ProgressConfig(
description: "Pushing image \(image.reference)",
itemsName: "blobs",
showItems: true,
showSpeed: false,
ignoreSmallSize: true
)
}
let progressConfig = try self.progressFlags.makeConfig(
description: "Pushing image \(image.reference)",
itemsName: "blobs",
showItems: true,
showSpeed: false,
ignoreSmallSize: true
)

let progress = ProgressBar(config: progressConfig)
defer {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Services/ContainerAPIService/Client/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,10 @@ public struct Flags {
public enum ProgressType: String, ExpressibleByArgument {
case none
case ansi
case plain
}

@Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi)", valueName: "type"))
@Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi|plain)", valueName: "type"))
public var progress: ProgressType = .ansi
}

Expand Down
1 change: 1 addition & 0 deletions Sources/TerminalProgress/ProgressBar+State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ extension ProgressBar {
}

var startTime: DispatchTime
var lastPlainRenderTime: DispatchTime?
var output = ""
var renderTask: Task<Void, Never>?

Expand Down
31 changes: 21 additions & 10 deletions Sources/TerminalProgress/ProgressBar+Terminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ extension ProgressBar {
public func clearAndResetCursor() {
state.withLock { s in
clear(state: &s)
resetCursor()
switch config.outputMode {
case .ansi:
resetCursor()
case .plain:
break
}
}
}

Expand Down Expand Up @@ -80,16 +85,22 @@ extension ProgressBar {
func displayText(_ text: String, state: inout State, terminating: String = "\r") {
state.output = text

// Clears previously printed lines.
var lines = ""
if terminating.hasSuffix("\r") && termWidth > 0 {
let lineCount = (text.count - 1) / termWidth
for _ in 0..<lineCount {
lines += EscapeSequence.moveUp
switch config.outputMode {
case .plain:
guard !text.isEmpty else { return }
display("\(text)\(terminating)")
case .ansi:
// Clears previously printed lines.
var lines = ""
if terminating.hasSuffix("\r") && termWidth > 0 {
let lineCount = (text.count - 1) / termWidth
for _ in 0..<lineCount {
lines += EscapeSequence.moveUp
}
}
}

let output = "\(text)\(EscapeSequence.clearToEndOfLine)\(terminating)\(lines)"
display(output)
let output = "\(text)\(EscapeSequence.clearToEndOfLine)\(terminating)\(lines)"
display(output)
}
}
}
36 changes: 32 additions & 4 deletions Sources/TerminalProgress/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,23 @@ public final class ProgressBar: Sendable {
/// - Parameter config: The configuration for the progress bar.
public init(config: ProgressConfig) {
self.config = config
term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil
switch config.outputMode {
case .ansi:
term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil
case .plain:
term = config.terminal
}
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)
switch config.outputMode {
case .ansi:
display(EscapeSequence.hideCursor)
case .plain:
break
}
}

/// Allows resetting the progress state.
Expand Down Expand Up @@ -129,7 +139,12 @@ public final class ProgressBar: Sendable {
if shouldClear {
clear(state: &s)
}
resetCursor()
switch config.outputMode {
case .ansi:
resetCursor()
case .plain:
break
}
}
}
}
Expand Down Expand Up @@ -157,8 +172,21 @@ extension ProgressBar {
guard force || !state.finished else {
return
}

if config.outputMode == .plain && !force {
let now = DispatchTime.now()
if let lastRender = state.lastPlainRenderTime {
let elapsed = now.uptimeNanoseconds - lastRender.uptimeNanoseconds
guard elapsed >= 1_000_000_000 else {
return
}
}
state.lastPlainRenderTime = now
}

let output = draw(state: state)
displayText(output, state: &state)
let terminating = config.outputMode == .plain ? "\n" : "\r"
displayText(output, state: &state, terminating: terminating)
}

/// Detail levels for progressive truncation.
Expand Down
16 changes: 15 additions & 1 deletion Sources/TerminalProgress/ProgressConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public struct ProgressConfig: Sendable {
public let clearOnFinish: Bool
/// The flag indicating whether to update the progress bar.
public let disableProgressUpdates: Bool
/// The output mode for progress rendering.
public let outputMode: OutputMode

/// Creates a new instance of `ProgressConfig`.
/// - Parameters:
/// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`.
Expand All @@ -88,6 +91,7 @@ public struct ProgressConfig: Sendable {
/// - theme: The theme of the progress bar. The default value is `nil`.
/// - clearOnFinish: The flag indicating whether to clear the progress bar before resetting the cursor. The default is `true`.
/// - disableProgressUpdates: The flag indicating whether to update the progress bar. The default is `false`.
/// - outputMode: The output mode for progress rendering. The default is `.ansi`.
public init(
terminal: FileHandle = .standardError,
description: String = "",
Expand All @@ -109,7 +113,8 @@ public struct ProgressConfig: Sendable {
width: Int = 120,
theme: ProgressTheme? = nil,
clearOnFinish: Bool = true,
disableProgressUpdates: Bool = false
disableProgressUpdates: Bool = false,
outputMode: OutputMode = .ansi
) throws {
if let totalTasks {
guard totalTasks > 0 else {
Expand Down Expand Up @@ -151,10 +156,19 @@ public struct ProgressConfig: Sendable {
self.theme = theme ?? DefaultProgressTheme()
self.clearOnFinish = clearOnFinish
self.disableProgressUpdates = disableProgressUpdates
self.outputMode = outputMode
}
}

extension ProgressConfig {
/// The output mode for progress rendering.
public enum OutputMode: Sendable {
/// ANSI escape code mode with cursor control and line overwriting.
case ansi
/// Plain text mode with newline-separated output, no ANSI codes.
case plain
}

/// An enumeration of errors that can occur when creating a `ProgressConfig`.
public enum Error: Swift.Error, CustomStringConvertible {
case invalid(String)
Expand Down
Loading
Loading