From 3c04f7fd18ffaebe80e2a5ab25bc6ed138e8c55e Mon Sep 17 00:00:00 2001 From: Karan Date: Wed, 19 Nov 2025 06:36:10 +0530 Subject: [PATCH 1/2] feat: color and plain modes for --progress flag Signed-off-by: Karan --- Sources/ContainerClient/Flags.swift | 4 ++- .../Container/ContainerRun.swift | 6 ++-- .../ContainerCommands/Image/ImagePull.swift | 6 ++-- .../ContainerCommands/Image/ImagePush.swift | 6 ++-- .../ProgressBar+Terminal.swift | 16 +++++++-- Sources/TerminalProgress/ProgressBar.swift | 35 +++++++++++++++---- Sources/TerminalProgress/ProgressConfig.swift | 16 +++++++-- docs/command-reference.md | 6 ++-- 8 files changed, 72 insertions(+), 23 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 4a5cfd27..c7b5f3d1 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -210,9 +210,11 @@ public struct Flags { public enum ProgressType: String, ExpressibleByArgument { case none case ansi + case plain + case color } - @Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi)", valueName: "type")) + @Option(name: .long, help: ArgumentHelp("Progress type (format: none|ansi|plain|color)", valueName: "type")) public var progress: ProgressType = .ansi } diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index 790f8064..560cf869 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -66,12 +66,14 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) - case .ansi: + case .ansi, .plain, .color: progressConfig = try ProgressConfig( showTasks: true, showItems: true, ignoreSmallSize: true, - totalTasks: 6 + totalTasks: 6, + color: self.progressFlags.progress == .color, + plain: self.progressFlags.progress == .plain ) } diff --git a/Sources/ContainerCommands/Image/ImagePull.swift b/Sources/ContainerCommands/Image/ImagePull.swift index 55ca722d..3fd7c762 100644 --- a/Sources/ContainerCommands/Image/ImagePull.swift +++ b/Sources/ContainerCommands/Image/ImagePull.swift @@ -83,12 +83,14 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) - case .ansi: + case .ansi, .plain, .color: progressConfig = try ProgressConfig( showTasks: true, showItems: true, ignoreSmallSize: true, - totalTasks: 2 + totalTasks: 2, + color: self.progressFlags.progress == .color, + plain: self.progressFlags.progress == .plain ) } diff --git a/Sources/ContainerCommands/Image/ImagePush.swift b/Sources/ContainerCommands/Image/ImagePush.swift index 3bd57f71..59596ebb 100644 --- a/Sources/ContainerCommands/Image/ImagePush.swift +++ b/Sources/ContainerCommands/Image/ImagePush.swift @@ -70,13 +70,15 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) - case .ansi: + case .ansi, .color, .plain: progressConfig = try ProgressConfig( description: "Pushing image \(image.reference)", itemsName: "blobs", showItems: true, showSpeed: false, - ignoreSmallSize: true + ignoreSmallSize: true, + color: self.progressFlags.progress == .color, + plain: self.progressFlags.progress == .plain ) } diff --git a/Sources/TerminalProgress/ProgressBar+Terminal.swift b/Sources/TerminalProgress/ProgressBar+Terminal.swift index ab997fea..9dd0a69f 100644 --- a/Sources/TerminalProgress/ProgressBar+Terminal.swift +++ b/Sources/TerminalProgress/ProgressBar+Terminal.swift @@ -63,12 +63,17 @@ extension ProgressBar { } func displayText(_ text: String, terminating: String = "\r") { + if config.plain { + display(text + "\n") + return + } var text = text + let visibleText = stripAnsi(text) // Clears previously printed characters if the new string is shorter. printedWidth.withLock { - text += String(repeating: " ", count: max($0 - text.count, 0)) - $0 = text.count + text += String(repeating: " ", count: max($0 - visibleText.count, 0)) + $0 = visibleText.count } state.withLock { $0.output = text @@ -77,7 +82,7 @@ extension ProgressBar { // Clears previously printed lines. var lines = "" if terminating.hasSuffix("\r") && terminalWidth > 0 { - let lineCount = (text.count - 1) / terminalWidth + let lineCount = (visibleText.count - 1) / terminalWidth for _ in 0.. String { + let pattern = #"\u001B\[[0-9;]*[A-Za-z]"# + return text.replacingOccurrences(of: pattern, with: "", options: .regularExpression) + } } diff --git a/Sources/TerminalProgress/ProgressBar.swift b/Sources/TerminalProgress/ProgressBar.swift index 61dca8ae..0feac374 100644 --- a/Sources/TerminalProgress/ProgressBar.swift +++ b/Sources/TerminalProgress/ProgressBar.swift @@ -72,6 +72,9 @@ public final class ProgressBar: Sendable { $0.subDescription = "" $0.tasks += 1 } + if config.plain { + render(force: true) + } } /// Updates the additional description of the progress bar. @@ -80,6 +83,9 @@ public final class ProgressBar: Sendable { resetCurrentTask() state.withLock { $0.subDescription = subDescription } + if config.plain { + render(force: true) + } } private func start(intervalSeconds: TimeInterval) async { @@ -96,6 +102,9 @@ public final class ProgressBar: Sendable { /// Starts an animation of the progress bar. /// - Parameter intervalSeconds: The time interval between updates in seconds. public func start(intervalSeconds: TimeInterval = 0.04) { + if config.plain { + return + } Task(priority: .utility) { await start(intervalSeconds: intervalSeconds) } @@ -148,15 +157,15 @@ extension ProgressBar { if config.showSpinner && !config.showProgressBar { if !state.finished { let spinnerIcon = config.theme.getSpinnerIcon(state.iteration) - components.append("\(spinnerIcon)") + components.append(colorize("\(spinnerIcon)", Color.cyan)) } else { - components.append("\(config.theme.done)") + components.append(colorize("\(config.theme.done)", Color.green)) } } if config.showTasks, let totalTasks = state.totalTasks { let tasks = min(state.tasks, totalTasks) - components.append("[\(tasks)/\(totalTasks)]") + components.append(colorize("[\(tasks)/\(totalTasks)]", Color.cyan)) } if config.showDescription && !state.description.isEmpty { @@ -172,7 +181,7 @@ extension ProgressBar { let total = state.totalSize ?? Int64(state.totalItems ?? 0) if config.showPercent && total > 0 && allowProgress { - components.append("\(state.finished ? "100%" : state.percent)") + components.append(colorize("\(state.finished ? "100%" : state.percent)", Color.green)) } if config.showProgressBar, total > 0, allowProgress { @@ -181,7 +190,7 @@ extension ProgressBar { 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)|") + components.append("|\(colorize(bar, Color.green))|") } var additionalComponents = [String]() @@ -246,13 +255,13 @@ extension ProgressBar { if additionalComponents.count > 0 { let joinedAdditionalComponents = additionalComponents.joined(separator: ", ") - components.append("(\(joinedAdditionalComponents))") + components.append("(\(colorize(joinedAdditionalComponents, Color.cyan)))") } if config.showTime { let timeDifferenceSeconds = secondsSinceStart() let formattedTime = timeDifferenceSeconds.formattedTime() - components.append("[\(formattedTime)]") + components.append("[\(colorize(formattedTime, Color.blue))]") } return components.joined(separator: " ") @@ -287,4 +296,16 @@ extension ProgressBar { } return "\(sizeNumber)/\(totalSizeNumber) \(totalSizeUnit)" } + + private func colorize(_ text: String, _ color: String) -> String { + guard config.color else { return text } + return "\(color)\(text)\(Color.reset)" + } + + private enum Color { + static let green = "\u{001B}[32m" + static let cyan = "\u{001B}[36m" + static let blue = "\u{001B}[34m" + static let reset = "\u{001B}[0m" + } } diff --git a/Sources/TerminalProgress/ProgressConfig.swift b/Sources/TerminalProgress/ProgressConfig.swift index 37eb107a..d3882fea 100644 --- a/Sources/TerminalProgress/ProgressConfig.swift +++ b/Sources/TerminalProgress/ProgressConfig.swift @@ -65,6 +65,10 @@ public struct ProgressConfig: Sendable { public let clearOnFinish: Bool /// The flag indicating whether to update the progress bar. public let disableProgressUpdates: Bool + /// The flag indicating whether to use colors. + public let color: Bool + /// The flag indicating whether to use plain output (no ANSI codes, one line per update). + public let plain: Bool /// Creates a new instance of `ProgressConfig`. /// - Parameters: /// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`. @@ -88,6 +92,8 @@ 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`. + /// - color: A flag indicating whether to enable ANSI color output. The default value is `false`. + /// - plain: A flag indicating whether to force plain output with no control sequences. The default value is `false`. public init( terminal: FileHandle = .standardError, description: String = "", @@ -109,7 +115,9 @@ public struct ProgressConfig: Sendable { width: Int = 120, theme: ProgressTheme? = nil, clearOnFinish: Bool = true, - disableProgressUpdates: Bool = false + disableProgressUpdates: Bool = false, + color: Bool = false, + plain: Bool = false ) throws { if let totalTasks { guard totalTasks > 0 else { @@ -132,11 +140,11 @@ public struct ProgressConfig: Sendable { self.initialSubDescription = subDescription self.initialItemsName = itemsName - self.showSpinner = showSpinner + self.showSpinner = plain ? false : showSpinner self.showTasks = showTasks self.showDescription = showDescription self.showPercent = showPercent - self.showProgressBar = showProgressBar + self.showProgressBar = plain ? false : showProgressBar self.showItems = showItems self.showSize = showSize self.showSpeed = showSpeed @@ -151,6 +159,8 @@ public struct ProgressConfig: Sendable { self.theme = theme ?? DefaultProgressTheme() self.clearOnFinish = clearOnFinish self.disableProgressUpdates = disableProgressUpdates + self.color = plain ? false : color + self.plain = plain } } diff --git a/docs/command-reference.md b/docs/command-reference.md index 2b105bf7..0dc39744 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -84,7 +84,7 @@ container run [] [ ...] **Progress Options** -* `--progress `: Progress type (format: none|ansi) (default: ansi) +* `--progress `: Progress type (format: none|ansi|plain|color) (default: ansi) **Examples** @@ -449,7 +449,7 @@ container image pull [--debug] [--scheme ] [--progress ] [--arch < **Options** * `--scheme `: Scheme to use when connecting to the container registry. One of (http, https, auto) (default: auto) -* `--progress `: Progress type (format: none|ansi) (default: ansi) +* `--progress `: Progress type (format: none|ansi|plain|color) (default: ansi) * `-a, --arch `: Limit the pull to the specified architecture * `--os `: Limit the pull to the specified OS * `--platform `: Limit the pull to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) @@ -471,7 +471,7 @@ container image push [--scheme ] [--progress ] [--arch ] [-- **Options** * `--scheme `: Scheme to use when connecting to the container registry. One of (http, https, auto) (default: auto) -* `--progress `: Progress type (format: none|ansi) (default: ansi) +* `--progress `: Progress type (format: none|ansi|plain|color) (default: ansi) * `-a, --arch `: Limit the push to the specified architecture * `--os `: Limit the push to the specified OS * `--platform `: Limit the push to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) From 7a8af882eef276d6b396e32840d4696717884ab6 Mon Sep 17 00:00:00 2001 From: Karan Date: Fri, 5 Dec 2025 13:25:00 +0530 Subject: [PATCH 2/2] refactor(progressconfig): convert bools to OutputMode enum Signed-off-by: Karan --- .../ContainerBuild/BuildImageResolver.swift | 2 +- .../Container/ContainerRun.swift | 5 ++- .../ContainerCommands/Image/ImagePull.swift | 5 ++- .../ContainerCommands/Image/ImagePush.swift | 5 ++- .../ProgressTypeExtension.swift | 30 ++++++++++++++++ .../ProgressBar+Terminal.swift | 2 +- Sources/TerminalProgress/ProgressBar.swift | 12 +++---- Sources/TerminalProgress/ProgressConfig.swift | 35 ++++++++++--------- 8 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 Sources/ContainerCommands/ProgressTypeExtension.swift diff --git a/Sources/ContainerBuild/BuildImageResolver.swift b/Sources/ContainerBuild/BuildImageResolver.swift index f0d45ee1..76601f55 100644 --- a/Sources/ContainerBuild/BuildImageResolver.swift +++ b/Sources/ContainerBuild/BuildImageResolver.swift @@ -66,7 +66,7 @@ struct BuildImageResolver: BuildPipelineHandler { showProgressBar: true, showSize: true, showSpeed: true, - disableProgressUpdates: self.quiet + outputMode: self.quiet ? .none : .ansi ) let progress = ProgressBar(config: progressConfig) defer { progress.finish() } diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index 560cf869..0f81bf33 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -65,15 +65,14 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { - case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) + case .none: progressConfig = try ProgressConfig(outputMode: .none) case .ansi, .plain, .color: progressConfig = try ProgressConfig( showTasks: true, showItems: true, ignoreSmallSize: true, totalTasks: 6, - color: self.progressFlags.progress == .color, - plain: self.progressFlags.progress == .plain + outputMode: self.progressFlags.progress.outputMode ) } diff --git a/Sources/ContainerCommands/Image/ImagePull.swift b/Sources/ContainerCommands/Image/ImagePull.swift index 3fd7c762..69780ec6 100644 --- a/Sources/ContainerCommands/Image/ImagePull.swift +++ b/Sources/ContainerCommands/Image/ImagePull.swift @@ -82,15 +82,14 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { - case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) + case .none: progressConfig = try ProgressConfig(outputMode: .none) case .ansi, .plain, .color: progressConfig = try ProgressConfig( showTasks: true, showItems: true, ignoreSmallSize: true, totalTasks: 2, - color: self.progressFlags.progress == .color, - plain: self.progressFlags.progress == .plain + outputMode: self.progressFlags.progress.outputMode ) } diff --git a/Sources/ContainerCommands/Image/ImagePush.swift b/Sources/ContainerCommands/Image/ImagePush.swift index 59596ebb..5f9d07c5 100644 --- a/Sources/ContainerCommands/Image/ImagePush.swift +++ b/Sources/ContainerCommands/Image/ImagePush.swift @@ -69,7 +69,7 @@ extension Application { var progressConfig: ProgressConfig switch self.progressFlags.progress { - case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) + case .none: progressConfig = try ProgressConfig(outputMode: .none) case .ansi, .color, .plain: progressConfig = try ProgressConfig( description: "Pushing image \(image.reference)", @@ -77,8 +77,7 @@ extension Application { showItems: true, showSpeed: false, ignoreSmallSize: true, - color: self.progressFlags.progress == .color, - plain: self.progressFlags.progress == .plain + outputMode: self.progressFlags.progress.outputMode ) } diff --git a/Sources/ContainerCommands/ProgressTypeExtension.swift b/Sources/ContainerCommands/ProgressTypeExtension.swift new file mode 100644 index 00000000..59d8585c --- /dev/null +++ b/Sources/ContainerCommands/ProgressTypeExtension.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 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 ContainerClient +import TerminalProgress + +extension Flags.Progress.ProgressType { + /// Converts the progress type to a ProgressConfig output mode. + var outputMode: ProgressConfig.OutputMode { + switch self { + case .none: .none + case .ansi: .ansi + case .plain: .plain + case .color: .color + } + } +} diff --git a/Sources/TerminalProgress/ProgressBar+Terminal.swift b/Sources/TerminalProgress/ProgressBar+Terminal.swift index 9dd0a69f..5f75ed28 100644 --- a/Sources/TerminalProgress/ProgressBar+Terminal.swift +++ b/Sources/TerminalProgress/ProgressBar+Terminal.swift @@ -63,7 +63,7 @@ extension ProgressBar { } func displayText(_ text: String, terminating: String = "\r") { - if config.plain { + if config.outputMode == .plain { display(text + "\n") return } diff --git a/Sources/TerminalProgress/ProgressBar.swift b/Sources/TerminalProgress/ProgressBar.swift index 0feac374..40f4a82c 100644 --- a/Sources/TerminalProgress/ProgressBar.swift +++ b/Sources/TerminalProgress/ProgressBar.swift @@ -72,7 +72,7 @@ public final class ProgressBar: Sendable { $0.subDescription = "" $0.tasks += 1 } - if config.plain { + if config.outputMode == .plain { render(force: true) } } @@ -83,7 +83,7 @@ public final class ProgressBar: Sendable { resetCurrentTask() state.withLock { $0.subDescription = subDescription } - if config.plain { + if config.outputMode == .plain { render(force: true) } } @@ -102,7 +102,7 @@ public final class ProgressBar: Sendable { /// Starts an animation of the progress bar. /// - Parameter intervalSeconds: The time interval between updates in seconds. public func start(intervalSeconds: TimeInterval = 0.04) { - if config.plain { + if config.outputMode == .plain { return } Task(priority: .utility) { @@ -121,7 +121,7 @@ public final class ProgressBar: Sendable { // The last render. render(force: true) - if !config.disableProgressUpdates && !config.clearOnFinish { + if config.outputMode != .none && !config.clearOnFinish { displayText(state.withLock { $0.output }, terminating: "\n") } @@ -143,7 +143,7 @@ extension ProgressBar { } func render(force: Bool = false) { - guard term != nil && !config.disableProgressUpdates && (force || !state.withLock { $0.finished }) else { + guard term != nil && config.outputMode != .none && (force || !state.withLock { $0.finished }) else { return } let output = draw() @@ -298,7 +298,7 @@ extension ProgressBar { } private func colorize(_ text: String, _ color: String) -> String { - guard config.color else { return text } + guard config.outputMode == .color else { return text } return "\(color)\(text)\(Color.reset)" } diff --git a/Sources/TerminalProgress/ProgressConfig.swift b/Sources/TerminalProgress/ProgressConfig.swift index d3882fea..1864f79a 100644 --- a/Sources/TerminalProgress/ProgressConfig.swift +++ b/Sources/TerminalProgress/ProgressConfig.swift @@ -18,6 +18,17 @@ import Foundation /// A configuration for displaying a progress bar. public struct ProgressConfig: Sendable { + /// The output mode for progress display. + public enum OutputMode: Sendable { + /// No progress output. + case none + /// ANSI control sequences without color (default). + case ansi + /// Plain text output, one line per update. + case plain + /// ANSI control sequences with color. + case color + } /// The file handle for progress updates. let terminal: FileHandle /// The initial description of the progress bar. @@ -63,12 +74,8 @@ public struct ProgressConfig: Sendable { public let theme: ProgressTheme /// The flag indicating whether to clear the progress bar before resetting the cursor. public let clearOnFinish: Bool - /// The flag indicating whether to update the progress bar. - public let disableProgressUpdates: Bool - /// The flag indicating whether to use colors. - public let color: Bool - /// The flag indicating whether to use plain output (no ANSI codes, one line per update). - public let plain: Bool + /// The output mode for progress display. + public let outputMode: OutputMode /// Creates a new instance of `ProgressConfig`. /// - Parameters: /// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`. @@ -91,9 +98,7 @@ public struct ProgressConfig: Sendable { /// - width: The width of the progress bar in characters. The default value is `120`. /// - 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`. - /// - color: A flag indicating whether to enable ANSI color output. The default value is `false`. - /// - plain: A flag indicating whether to force plain output with no control sequences. The default value is `false`. + /// - outputMode: The output mode for progress display. The default value is `.ansi`. public init( terminal: FileHandle = .standardError, description: String = "", @@ -115,9 +120,7 @@ public struct ProgressConfig: Sendable { width: Int = 120, theme: ProgressTheme? = nil, clearOnFinish: Bool = true, - disableProgressUpdates: Bool = false, - color: Bool = false, - plain: Bool = false + outputMode: OutputMode = .ansi ) throws { if let totalTasks { guard totalTasks > 0 else { @@ -140,11 +143,11 @@ public struct ProgressConfig: Sendable { self.initialSubDescription = subDescription self.initialItemsName = itemsName - self.showSpinner = plain ? false : showSpinner + self.showSpinner = (outputMode == .plain || outputMode == .none) ? false : showSpinner self.showTasks = showTasks self.showDescription = showDescription self.showPercent = showPercent - self.showProgressBar = plain ? false : showProgressBar + self.showProgressBar = (outputMode == .plain || outputMode == .none) ? false : showProgressBar self.showItems = showItems self.showSize = showSize self.showSpeed = showSpeed @@ -158,9 +161,7 @@ public struct ProgressConfig: Sendable { self.width = width self.theme = theme ?? DefaultProgressTheme() self.clearOnFinish = clearOnFinish - self.disableProgressUpdates = disableProgressUpdates - self.color = plain ? false : color - self.plain = plain + self.outputMode = outputMode } }