@@ -34,7 +34,7 @@ public final class ProgressBar: Sendable {
3434 public init ( config: ProgressConfig ) {
3535 self . config = config
3636 switch config. outputMode {
37- case . ansi:
37+ case . ansi, . color :
3838 term = isatty ( config. terminal. fileDescriptor) == 1 ? config. terminal : nil
3939 case . plain:
4040 term = config. terminal
@@ -45,7 +45,7 @@ public final class ProgressBar: Sendable {
4545 totalSize: config. initialTotalSize)
4646 self . state = Mutex ( state)
4747 switch config. outputMode {
48- case . ansi:
48+ case . ansi, . color :
4949 display ( EscapeSequence . hideCursor)
5050 case . plain:
5151 break
@@ -140,7 +140,7 @@ public final class ProgressBar: Sendable {
140140 clear ( state: & s)
141141 }
142142 switch config. outputMode {
143- case . ansi:
143+ case . ansi, . color :
144144 resetCursor ( )
145145 case . plain:
146146 break
@@ -213,7 +213,8 @@ extension ProgressBar {
213213
214214 for detail in DetailLevel . allCases {
215215 let output = draw ( state: state, detail: detail)
216- if output. count <= targetWidth {
216+ let length = config. outputMode == . color ? output. visibleLength : output. count
217+ if length <= targetWidth {
217218 return output
218219 }
219220 }
@@ -222,30 +223,37 @@ extension ProgressBar {
222223 }
223224
224225 func draw( state: State , detail: DetailLevel ) -> String {
226+ let useColor = config. outputMode == . color
227+
228+ /// Wraps text in ANSI color when color mode is active; returns text unchanged otherwise.
229+ func colored( _ text: String , _ code: String ) -> String {
230+ useColor ? EscapeSequence . colored ( text, code) : text
231+ }
232+
225233 var components = [ String] ( )
226234
227235 // Spinner - always shown if configured (unless using progress bar)
228236 if config. showSpinner && !config. showProgressBar {
229237 if !state. finished {
230238 let spinnerIcon = config. theme. getSpinnerIcon ( state. iteration)
231- components. append ( " \( spinnerIcon) " )
239+ components. append ( colored ( " \( spinnerIcon) " , EscapeSequence . cyan ) )
232240 } else {
233- components. append ( " \( config. theme. done) " )
241+ components. append ( colored ( " \( config. theme. done) " , EscapeSequence . green ) )
234242 }
235243 }
236244
237245 // Tasks [x/y] - always shown if configured
238246 if config. showTasks, let totalTasks = state. totalTasks {
239247 let tasks = min ( state. tasks, totalTasks)
240- components. append ( " [ \( tasks) / \( totalTasks) ] " )
248+ components. append ( colored ( " [ \( tasks) / \( totalTasks) ] " , EscapeSequence . cyan ) )
241249 }
242250
243251 // Description - dropped at noDescription level
244252 if detail. rawValue < DetailLevel . noDescription. rawValue {
245253 if config. showDescription && !state. description. isEmpty {
246- components. append ( " \( state. description) " )
254+ components. append ( colored ( " \( state. description) " , EscapeSequence . bold ) )
247255 if !state. subDescription. isEmpty {
248- components. append ( " \( state. subDescription) " )
256+ components. append ( colored ( " \( state. subDescription) " , EscapeSequence . bold ) )
249257 }
250258 }
251259 }
@@ -256,17 +264,27 @@ extension ProgressBar {
256264
257265 // Percent - always shown if configured
258266 if config. showPercent && total > 0 && allowProgress {
259- components. append ( " \( state. finished ? " 100% " : state. percent) " )
267+ let percentText = state. finished ? " 100% " : state. percent
268+ let percentColor = state. finished ? EscapeSequence . green : EscapeSequence . yellow
269+ components. append ( colored ( percentText, percentColor) )
260270 }
261271
262272 // Progress bar - always shown if configured
263273 if config. showProgressBar, total > 0 , allowProgress {
264- let usedWidth = components. joined ( separator: " " ) . count + 45
274+ let joinedComponents = components. joined ( separator: " " )
275+ // 45 reserves space for components rendered after the bar (size, speed, time, etc.)
276+ let usedWidth = ( useColor ? joinedComponents. visibleLength : joinedComponents. count) + 45
265277 let remainingWidth = max ( config. width - usedWidth, 1 )
266278 let barLength = min ( remainingWidth, state. finished ? remainingWidth : Int ( Int64 ( remainingWidth) * value / total) )
267279 let barPaddingLength = remainingWidth - barLength
268- let bar = " \( String ( repeating: config. theme. bar, count: barLength) ) \( String ( repeating: " " , count: barPaddingLength) ) "
269- components. append ( " | \( bar) | " )
280+ if useColor {
281+ let filledBar = EscapeSequence . colored ( String ( repeating: config. theme. bar, count: barLength) , EscapeSequence . green)
282+ let emptyBar = String ( repeating: " " , count: barPaddingLength)
283+ components. append ( " | \( filledBar) \( emptyBar) | " )
284+ } else {
285+ let bar = " \( String ( repeating: config. theme. bar, count: barLength) ) \( String ( repeating: " " , count: barPaddingLength) ) "
286+ components. append ( " | \( bar) | " )
287+ }
270288 }
271289
272290 // Additional components in parens - progressively dropped
@@ -340,15 +358,15 @@ extension ProgressBar {
340358
341359 if additionalComponents. count > 0 {
342360 let joinedAdditionalComponents = additionalComponents. joined ( separator: " , " )
343- components. append ( " ( \( joinedAdditionalComponents) ) " )
361+ components. append ( colored ( " ( \( joinedAdditionalComponents) ) " , EscapeSequence . dim ) )
344362 }
345363 }
346364
347365 // Time - dropped at noTime level
348366 if detail. rawValue < DetailLevel . noTime. rawValue && config. showTime {
349367 let timeDifferenceSeconds = secondsSinceStart ( from: state. startTime)
350368 let formattedTime = timeDifferenceSeconds. formattedTime ( )
351- components. append ( " [ \( formattedTime) ] " )
369+ components. append ( colored ( " [ \( formattedTime) ] " , EscapeSequence . dim ) )
352370 }
353371
354372 return components. joined ( separator: " " )
0 commit comments