• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jedib0t / go-pretty / 16516871092

25 Jul 2025 07:53AM UTC coverage: 99.773% (-0.05%) from 99.823%
16516871092

Pull #369

github

ptxmac
Experimental Style funcs for progress
Pull Request #369: Experimental Style funcs for progress

1 of 3 new or added lines in 1 file covered. (33.33%)

3953 of 3962 relevant lines covered (99.77%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.47
/progress/render.go
1
package progress
2

3
import (
4
        "fmt"
5
        "math"
6
        "strings"
7
        "time"
8

9
        "github.com/jedib0t/go-pretty/v6/text"
10
)
11

12
// Render renders the Progress tracker and handles all existing trackers and
13
// those that are added dynamically while render is in progress.
14
func (p *Progress) Render() {
1✔
15
        if p.beginRender() {
2✔
16
                p.initForRender()
1✔
17

1✔
18
                lastRenderLength := 0
1✔
19
                ticker := time.NewTicker(p.updateFrequency)
1✔
20
                defer ticker.Stop()
1✔
21
                for {
2✔
22
                        select {
1✔
23
                        case <-ticker.C:
1✔
24
                                lastRenderLength = p.renderTrackers(lastRenderLength)
1✔
25
                        case <-p.renderContext.Done():
1✔
26
                                // always render the current state before finishing render in
1✔
27
                                // case it hasn't been shown yet
1✔
28
                                p.renderTrackers(lastRenderLength)
1✔
29
                                p.endRender()
1✔
30
                                return
1✔
31
                        }
32
                }
33
        }
34
}
35

36
func (p *Progress) beginRender() bool {
1✔
37
        p.renderInProgressMutex.Lock()
1✔
38
        defer p.renderInProgressMutex.Unlock()
1✔
39

1✔
40
        if p.renderInProgress {
2✔
41
                return false
1✔
42
        }
1✔
43
        p.renderInProgress = true
1✔
44
        return true
1✔
45
}
46

47
func (p *Progress) consumeQueuedTrackers() {
1✔
48
        if p.LengthInQueue() > 0 {
2✔
49
                p.trackersActiveMutex.Lock()
1✔
50
                p.trackersInQueueMutex.Lock()
1✔
51
                p.trackersActive = append(p.trackersActive, p.trackersInQueue...)
1✔
52
                p.trackersInQueue = make([]*Tracker, 0)
1✔
53
                p.trackersInQueueMutex.Unlock()
1✔
54
                p.trackersActiveMutex.Unlock()
1✔
55
        }
1✔
56
}
57

58
func (p *Progress) endRender() {
1✔
59
        p.renderInProgressMutex.Lock()
1✔
60
        defer p.renderInProgressMutex.Unlock()
1✔
61

1✔
62
        p.renderInProgress = false
1✔
63
}
1✔
64

65
func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
1✔
66
        // move trackers waiting in queue to the active list
1✔
67
        p.consumeQueuedTrackers()
1✔
68

1✔
69
        // separate the active and done trackers
1✔
70
        var trackersActive, trackersDone []*Tracker
1✔
71
        var activeTrackersProgress int64
1✔
72
        p.trackersActiveMutex.RLock()
1✔
73
        var maxETA time.Duration
1✔
74
        for _, tracker := range p.trackersActive {
2✔
75
                if !tracker.IsDone() {
2✔
76
                        trackersActive = append(trackersActive, tracker)
1✔
77
                        activeTrackersProgress += int64(tracker.PercentDone())
1✔
78
                        if eta := tracker.ETA(); eta > maxETA {
2✔
79
                                maxETA = eta
1✔
80
                        }
1✔
81
                } else if !tracker.RemoveOnCompletion {
2✔
82
                        trackersDone = append(trackersDone, tracker)
1✔
83
                }
1✔
84
        }
85
        p.trackersActiveMutex.RUnlock()
1✔
86
        p.sortBy.Sort(trackersDone)
1✔
87
        p.sortBy.Sort(trackersActive)
1✔
88

1✔
89
        // calculate the overall tracker's progress value
1✔
90
        p.overallTracker.value = int64(p.LengthDone()+len(trackersDone)) * 100
1✔
91
        p.overallTracker.value += activeTrackersProgress
1✔
92
        p.overallTracker.minETA = maxETA
1✔
93
        if len(trackersActive) == 0 {
2✔
94
                p.overallTracker.MarkAsDone()
1✔
95
        }
1✔
96
        return trackersActive, trackersDone
1✔
97
}
98

99
func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string {
1✔
100
        value, total := t.valueAndTotal()
1✔
101
        if !hint.isOverallTracker && t.IsStarted() && (total == 0 || value > total) {
2✔
102
                return p.generateTrackerStrIndeterminate(maxLen)
1✔
103
        }
1✔
104
        return p.generateTrackerStrDeterminate(value, total, maxLen)
1✔
105
}
106

107
// generateTrackerStrDeterminate generates the tracker string for the case where
108
// the Total value is known, and the progress percentage can be calculated.
109
func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLen int) string {
1✔
110
        if p.style.CustomFuncs.TrackerDeterminate != nil {
1✔
NEW
111
                return p.style.CustomFuncs.TrackerDeterminate(value, total, maxLen)
×
NEW
112
        }
×
113

114
        pFinishedDots, pFinishedDotsFraction := 0.0, 0.0
1✔
115
        pDotValue := float64(total) / float64(maxLen)
1✔
116
        if pDotValue > 0 {
2✔
117
                pFinishedDots = float64(value) / pDotValue
1✔
118
                pFinishedDotsFraction = pFinishedDots - float64(int(pFinishedDots))
1✔
119
        }
1✔
120
        pFinishedLen := int(math.Floor(pFinishedDots))
1✔
121

1✔
122
        var pFinished, pInProgress, pUnfinished string
1✔
123
        if pFinishedLen > 0 {
2✔
124
                pFinished = strings.Repeat(p.style.Chars.Finished, pFinishedLen)
1✔
125
        }
1✔
126
        pInProgress = p.style.Chars.Unfinished
1✔
127
        if pFinishedDotsFraction >= 0.75 {
2✔
128
                pInProgress = p.style.Chars.Finished75
1✔
129
        } else if pFinishedDotsFraction >= 0.50 {
3✔
130
                pInProgress = p.style.Chars.Finished50
1✔
131
        } else if pFinishedDotsFraction >= 0.25 {
3✔
132
                pInProgress = p.style.Chars.Finished25
1✔
133
        } else if pFinishedDotsFraction == 0 {
3✔
134
                pInProgress = ""
1✔
135
        }
1✔
136
        pFinishedStrLen := text.StringWidthWithoutEscSequences(pFinished + pInProgress)
1✔
137
        if pFinishedStrLen < maxLen {
2✔
138
                pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen)
1✔
139
        }
1✔
140
        
141
        return p.style.Colors.Tracker.Sprintf("%s%s%s%s%s",
1✔
142
                p.style.Chars.BoxLeft, pFinished, pInProgress, pUnfinished, p.style.Chars.BoxRight,
1✔
143
        )
1✔
144
}
145

146
// generateTrackerStrIndeterminate generates the tracker string for the case where
147
// the Total value is unknown, and the progress percentage cannot be calculated.
148
func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string {
1✔
149
        indicator := p.style.Chars.Indeterminate(maxLen)
1✔
150

1✔
151
        pUnfinished := ""
1✔
152
        if indicator.Position > 0 {
2✔
153
                pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position)
1✔
154
        }
1✔
155
        pUnfinished += indicator.Text
1✔
156
        if text.StringWidthWithoutEscSequences(pUnfinished) < maxLen {
2✔
157
                pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.StringWidthWithoutEscSequences(pUnfinished))
1✔
158
        }
1✔
159

160
        return p.style.Colors.Tracker.Sprintf("%s%s%s",
1✔
161
                p.style.Chars.BoxLeft, pUnfinished, p.style.Chars.BoxRight,
1✔
162
        )
1✔
163
}
164

165
func (p *Progress) moveCursorToTheTop(out *strings.Builder) {
1✔
166
        numLinesToMoveUp := len(p.trackersActive)
1✔
167
        if p.style.Visibility.TrackerOverall && p.overallTracker != nil && !p.overallTracker.IsDone() {
2✔
168
                numLinesToMoveUp++
1✔
169
        }
1✔
170
        if p.style.Visibility.Pinned {
2✔
171
                numLinesToMoveUp += p.pinnedMessageNumLines
1✔
172
        }
1✔
173
        for numLinesToMoveUp > 0 {
2✔
174
                out.WriteString(text.CursorUp.Sprint())
1✔
175
                out.WriteString(text.EraseLine.Sprint())
1✔
176
                numLinesToMoveUp--
1✔
177
        }
1✔
178
}
179

180
func (p *Progress) renderPinnedMessages(out *strings.Builder) {
1✔
181
        p.pinnedMessageMutex.RLock()
1✔
182
        defer p.pinnedMessageMutex.RUnlock()
1✔
183

1✔
184
        numLines := len(p.pinnedMessages)
1✔
185
        for _, msg := range p.pinnedMessages {
2✔
186
                msg = strings.TrimSpace(msg)
1✔
187
                msg = p.style.Colors.Pinned.Sprint(msg)
1✔
188
                if width := p.getTerminalWidth(); width > 0 {
2✔
189
                        msg = text.Trim(msg, width)
1✔
190
                }
1✔
191
                out.WriteString(msg)
1✔
192
                out.WriteRune('\n')
1✔
193

1✔
194
                numLines += strings.Count(msg, "\n")
1✔
195
        }
196
        p.pinnedMessageNumLines = numLines
1✔
197
}
198

199
func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
200
        message := t.message()
1✔
201
        message = strings.ReplaceAll(message, "\t", "    ")
1✔
202
        message = strings.ReplaceAll(message, "\r", "") // replace with text.ProcessCRLF?
1✔
203
        if p.lengthMessage > 0 {
2✔
204
                messageLen := text.StringWidthWithoutEscSequences(message)
1✔
205
                if messageLen < p.lengthMessage {
2✔
206
                        message = text.Pad(message, p.lengthMessage, ' ')
1✔
207
                } else {
2✔
208
                        message = text.Snip(message, p.lengthMessage, p.style.Options.SnipIndicator)
1✔
209
                }
1✔
210
        }
211

212
        tOut := &strings.Builder{}
1✔
213
        tOut.Grow(p.lengthProgressOverall)
1✔
214
        if hint.isOverallTracker {
2✔
215
                if !t.IsDone() {
2✔
216
                        hint := renderHint{hideValue: true, isOverallTracker: true}
1✔
217
                        p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
1✔
218
                }
1✔
219
        } else {
1✔
220
                if t.IsDone() {
2✔
221
                        p.renderTrackerDone(tOut, t, message)
1✔
222
                } else {
2✔
223
                        hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value}
1✔
224
                        p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
1✔
225
                }
1✔
226
        }
227

228
        outStr := tOut.String()
1✔
229
        if width := p.getTerminalWidth(); width > 0 {
2✔
230
                outStr = text.Trim(outStr, width)
1✔
231
        }
1✔
232
        out.WriteString(outStr)
1✔
233
        out.WriteRune('\n')
1✔
234
}
235

236
func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message string) {
1✔
237
        if !t.RemoveOnCompletion {
2✔
238
                out.WriteString(p.style.Colors.Message.Sprint(message))
1✔
239
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
240
                if !t.IsErrored() {
2✔
241
                        out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.DoneString))
1✔
242
                } else {
2✔
243
                        out.WriteString(p.style.Colors.Error.Sprint(p.style.Options.ErrorString))
1✔
244
                }
1✔
245
                p.renderTrackerStats(out, t, renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value})
1✔
246
        }
247
}
248

249
func (p *Progress) renderTrackerMessage(out *strings.Builder, t *Tracker, message string) {
1✔
250
        if !t.IsErrored() {
2✔
251
                out.WriteString(p.style.Colors.Message.Sprint(message))
1✔
252
        } else {
2✔
253
                out.WriteString(p.style.Colors.Error.Sprint(message))
1✔
254
        }
1✔
255
}
256

257
func (p *Progress) renderTrackerPercentage(out *strings.Builder, t *Tracker) {
1✔
258
        if p.style.Visibility.Percentage {
2✔
259
                var percentageStr string
1✔
260
                if t.IsIndeterminate() {
2✔
261
                        percentageStr = p.style.Options.PercentIndeterminate
1✔
262
                } else {
2✔
263
                        percentageStr = fmt.Sprintf(p.style.Options.PercentFormat, t.PercentDone())
1✔
264
                }
1✔
265
                out.WriteString(p.style.Colors.Percent.Sprint(percentageStr))
1✔
266
        }
267
}
268

269
func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, message string, trackerStr string, hint renderHint) {
1✔
270
        if hint.isOverallTracker {
2✔
271
                out.WriteString(p.style.Colors.Tracker.Sprint(trackerStr))
1✔
272
                p.renderTrackerStats(out, t, hint)
1✔
273
        } else if p.trackerPosition == PositionRight {
3✔
274
                p.renderTrackerMessage(out, t, message)
1✔
275
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
276
                p.renderTrackerPercentage(out, t)
1✔
277
                if p.style.Visibility.Tracker {
2✔
278
                        out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
1✔
279
                }
1✔
280
                p.renderTrackerStats(out, t, hint)
1✔
281
        } else {
1✔
282
                p.renderTrackerPercentage(out, t)
1✔
283
                if p.style.Visibility.Tracker {
2✔
284
                        out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
1✔
285
                }
1✔
286
                p.renderTrackerStats(out, t, hint)
1✔
287
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
288
                p.renderTrackerMessage(out, t, message)
1✔
289
        }
290
}
291

292
func (p *Progress) renderTrackers(lastRenderLength int) int {
1✔
293
        if p.LengthActive() == 0 {
2✔
294
                return 0
1✔
295
        }
1✔
296

297
        // buffer all output into a strings.Builder object
298
        var out strings.Builder
1✔
299
        out.Grow(lastRenderLength)
1✔
300

1✔
301
        // move up N times based on the number of active trackers
1✔
302
        if lastRenderLength > 0 {
2✔
303
                p.moveCursorToTheTop(&out)
1✔
304
        }
1✔
305

306
        // render the trackers that are done, and then the ones that are active
307
        p.renderTrackersDoneAndActive(&out)
1✔
308

1✔
309
        // render the overall tracker
1✔
310
        if p.style.Visibility.TrackerOverall {
2✔
311
                p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true})
1✔
312
        }
1✔
313

314
        // write the text to the output writer
315
        _, _ = p.outputWriter.Write([]byte(out.String()))
1✔
316

1✔
317
        // stop if auto stop is enabled and there are no more active trackers
1✔
318
        if p.autoStop && p.LengthActive() == 0 {
2✔
319
                p.renderContextCancel()
1✔
320
        }
1✔
321

322
        return out.Len()
1✔
323
}
324

325
func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder) {
1✔
326
        // find the currently "active" and "done" trackers
1✔
327
        trackersActive, trackersDone := p.extractDoneAndActiveTrackers()
1✔
328

1✔
329
        // sort and render the done trackers
1✔
330
        for _, tracker := range trackersDone {
2✔
331
                p.renderTracker(out, tracker, renderHint{})
1✔
332
        }
1✔
333
        p.trackersDoneMutex.Lock()
1✔
334
        p.trackersDone = append(p.trackersDone, trackersDone...)
1✔
335
        p.trackersDoneMutex.Unlock()
1✔
336

1✔
337
        // render all the logs received and flush them out
1✔
338
        p.logsToRenderMutex.Lock()
1✔
339
        for _, log := range p.logsToRender {
2✔
340
                out.WriteString(text.EraseLine.Sprint())
1✔
341
                out.WriteString(log)
1✔
342
                out.WriteRune('\n')
1✔
343
        }
1✔
344
        p.logsToRender = nil
1✔
345
        p.logsToRenderMutex.Unlock()
1✔
346

1✔
347
        // render pinned messages
1✔
348
        if len(trackersActive) > 0 && p.style.Visibility.Pinned {
2✔
349
                p.renderPinnedMessages(out)
1✔
350
        }
1✔
351

352
        // sort and render the active trackers
353
        for _, tracker := range trackersActive {
2✔
354
                p.renderTracker(out, tracker, renderHint{})
1✔
355
        }
1✔
356
        p.trackersActiveMutex.Lock()
1✔
357
        p.trackersActive = trackersActive
1✔
358
        p.trackersActiveMutex.Unlock()
1✔
359
}
360

361
func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
362
        if !hint.hideValue || !hint.hideTime {
2✔
363
                var outStats strings.Builder
1✔
364
                outStats.WriteString(" [")
1✔
365

1✔
366
                if p.style.Options.SpeedPosition == PositionLeft {
2✔
367
                        p.renderTrackerStatsSpeed(&outStats, t, hint)
1✔
368
                }
1✔
369
                if !hint.hideValue {
2✔
370
                        outStats.WriteString(p.style.Colors.Value.Sprint(t.Units.Sprint(t.Value())))
1✔
371
                }
1✔
372
                if !hint.hideValue && !hint.hideTime {
2✔
373
                        outStats.WriteString(" in ")
1✔
374
                }
1✔
375
                if !hint.hideTime {
2✔
376
                        p.renderTrackerStatsTime(&outStats, t, hint)
1✔
377
                }
1✔
378
                if p.style.Options.SpeedPosition == PositionRight {
2✔
379
                        p.renderTrackerStatsSpeed(&outStats, t, hint)
1✔
380
                }
1✔
381
                outStats.WriteRune(']')
1✔
382

1✔
383
                out.WriteString(p.style.Colors.Stats.Sprint(outStats.String()))
1✔
384
        }
385
}
386

387
func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
388
        if hint.isOverallTracker && !p.style.Visibility.SpeedOverall {
2✔
389
                return
1✔
390
        }
1✔
391
        if !hint.isOverallTracker && !p.style.Visibility.Speed {
2✔
392
                return
1✔
393
        }
1✔
394

395
        speedPrecision := p.style.Options.SpeedPrecision
1✔
396
        if hint.isOverallTracker {
2✔
397
                speed := float64(0)
1✔
398

1✔
399
                p.trackersActiveMutex.RLock()
1✔
400
                for _, tracker := range p.trackersActive {
2✔
401
                        if !tracker.timeStart.IsZero() {
2✔
402
                                speed += float64(tracker.Value()) / time.Since(tracker.timeStart).Round(speedPrecision).Seconds()
1✔
403
                        }
1✔
404
                }
405
                p.trackersActiveMutex.RUnlock()
1✔
406

1✔
407
                if speed > 0 {
2✔
408
                        p.renderTrackerStatsSpeedInternal(out, p.style.Options.SpeedOverallFormatter(int64(speed)))
1✔
409
                }
1✔
410
        } else if !t.timeStart.IsZero() {
2✔
411
                timeTaken := time.Since(t.timeStart)
1✔
412
                if timeTakenRounded := timeTaken.Round(speedPrecision); timeTakenRounded > speedPrecision {
2✔
413
                        p.renderTrackerStatsSpeedInternal(out, t.Units.Sprint(int64(float64(t.Value())/timeTakenRounded.Seconds())))
1✔
414
                }
1✔
415
        }
416
}
417

418
func (p *Progress) renderTrackerStatsSpeedInternal(out *strings.Builder, speed string) {
1✔
419
        if p.style.Options.SpeedPosition == PositionRight {
2✔
420
                out.WriteString("; ")
1✔
421
        }
1✔
422
        out.WriteString(p.style.Colors.Speed.Sprint(speed))
1✔
423
        out.WriteString(p.style.Options.SpeedSuffix)
1✔
424
        if p.style.Options.SpeedPosition == PositionLeft {
2✔
425
                out.WriteString("; ")
1✔
426
        }
1✔
427
}
428

429
func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) {
1✔
430
        var td, tp time.Duration
1✔
431
        if !t.timeStart.IsZero() {
2✔
432
                if t.IsDone() {
2✔
433
                        td = t.timeStop.Sub(t.timeStart)
1✔
434
                } else {
2✔
435
                        td = time.Since(t.timeStart)
1✔
436
                }
1✔
437
        }
438
        if hint.isOverallTracker {
2✔
439
                tp = p.style.Options.TimeOverallPrecision
1✔
440
        } else if t.IsDone() {
3✔
441
                tp = p.style.Options.TimeDonePrecision
1✔
442
        } else {
2✔
443
                tp = p.style.Options.TimeInProgressPrecision
1✔
444
        }
1✔
445
        outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp)))
1✔
446

1✔
447
        p.renderTrackerStatsETA(outStats, t, hint)
1✔
448
}
449

450
func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
451
        if hint.isOverallTracker && !p.style.Visibility.ETAOverall {
2✔
452
                return
1✔
453
        }
1✔
454
        if !hint.isOverallTracker && !p.style.Visibility.ETA {
2✔
455
                return
1✔
456
        }
1✔
457

458
        tpETA := p.style.Options.ETAPrecision
1✔
459
        if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA {
2✔
460
                out.WriteString("; ")
1✔
461
                out.WriteString(p.style.Options.ETAString)
1✔
462
                out.WriteString(": ")
1✔
463
                out.WriteString(p.style.Colors.Time.Sprint(eta))
1✔
464
        }
1✔
465
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc