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

jedib0t / go-pretty / 27315107444

11 Jun 2026 12:18AM UTC coverage: 99.599% (-0.04%) from 99.638%
27315107444

push

github

jedib0t
table: guard auto-index render against an empty maxColumnLengths

renderColumnAutoIndex indexed maxColumnLengths[0] without a length
check. Not reachable through the public API today (verified empty,
all-hidden, suppressed and separator-only tables), but guard it as
defense-in-depth and lock the edge cases in with regression tests.

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

9 existing lines in 2 files now uncovered.

4973 of 4993 relevant lines covered (99.6%)

1.2 hits per line

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

96.7
/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) collectActiveTrackers() ([]*Tracker, int64, time.Duration) {
1✔
48
        var allTrackers []*Tracker
1✔
49
        var activeTrackersProgress int64
1✔
50
        var maxETA time.Duration
1✔
51

1✔
52
        p.trackersDoneMutex.RLock()
1✔
53
        lengthDone := len(p.trackersDone)
1✔
54
        p.trackersDoneMutex.RUnlock()
1✔
55

1✔
56
        p.trackersActiveMutex.RLock()
1✔
57
        allTrackers = make([]*Tracker, 0, len(p.trackersActive)+lengthDone)
1✔
58
        for _, tracker := range p.trackersActive {
2✔
59
                if !tracker.IsDone() || !tracker.RemoveOnCompletion {
2✔
60
                        allTrackers = append(allTrackers, tracker)
1✔
61
                        if !tracker.IsDone() {
2✔
62
                                activeTrackersProgress += int64(tracker.PercentDone())
1✔
63
                                if eta := tracker.ETA(); eta > maxETA {
2✔
64
                                        maxETA = eta
1✔
65
                                }
1✔
66
                        }
67
                }
68
        }
69
        p.trackersActiveMutex.RUnlock()
1✔
70

1✔
71
        return allTrackers, activeTrackersProgress, maxETA
1✔
72
}
73

74
func (p *Progress) collectDoneTrackers(allTrackers *[]*Tracker) {
1✔
75
        p.trackersDoneMutex.RLock()
1✔
76
        for _, tracker := range p.trackersDone {
1✔
77
                if !tracker.RemoveOnCompletion {
×
78
                        *allTrackers = append(*allTrackers, tracker)
×
79
                }
×
80
        }
81
        p.trackersDoneMutex.RUnlock()
1✔
82
}
83

84
func (p *Progress) consumeQueuedTrackers() {
1✔
85
        p.trackersInQueueMutex.Lock()
1✔
86
        queueLen := len(p.trackersInQueue)
1✔
87
        if queueLen == 0 {
2✔
88
                p.trackersInQueueMutex.Unlock()
1✔
89
                return
1✔
90
        }
1✔
91
        // copy the slice to avoid race condition - another goroutine may append
92
        // to p.trackersInQueue while we're appending to p.trackersActive
93
        queued := make([]*Tracker, len(p.trackersInQueue))
1✔
94
        copy(queued, p.trackersInQueue)
1✔
95
        p.trackersInQueue = p.trackersInQueue[:0] // reuse slice capacity
1✔
96
        p.trackersInQueueMutex.Unlock()
1✔
97

1✔
98
        p.trackersActiveMutex.Lock()
1✔
99
        p.trackersActive = append(p.trackersActive, queued...)
1✔
100
        p.trackersActiveMutex.Unlock()
1✔
101
}
102

103
func (p *Progress) endRender() {
1✔
104
        p.renderInProgressMutex.Lock()
1✔
105
        defer p.renderInProgressMutex.Unlock()
1✔
106

1✔
107
        p.renderInProgress = false
1✔
108
}
1✔
109

110
// extractDoneAndActiveTrackers walks p.trackersActive and partitions it into
111
// (stillActive, newlyDone). Used by the default scrollback render path; mirrors
112
// the pre-v6.7.8 behavior where done trackers exit the rewindable region on
113
// completion.
114
func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
1✔
115
        p.consumeQueuedTrackers()
1✔
116

1✔
117
        var trackersActive, trackersNewlyDone []*Tracker
1✔
118
        var activeTrackersProgress int64
1✔
119
        var maxETA time.Duration
1✔
120

1✔
121
        p.trackersDoneMutex.RLock()
1✔
122
        lengthDone := len(p.trackersDone)
1✔
123
        p.trackersDoneMutex.RUnlock()
1✔
124

1✔
125
        p.trackersActiveMutex.RLock()
1✔
126
        trackersActive = make([]*Tracker, 0, len(p.trackersActive))
1✔
127
        trackersNewlyDone = make([]*Tracker, 0, len(p.trackersActive)/4)
1✔
128
        for _, tracker := range p.trackersActive {
2✔
129
                if !tracker.IsDone() {
2✔
130
                        trackersActive = append(trackersActive, tracker)
1✔
131
                        activeTrackersProgress += int64(tracker.PercentDone())
1✔
132
                        if eta := tracker.ETA(); eta > maxETA {
2✔
133
                                maxETA = eta
1✔
134
                        }
1✔
135
                } else if !tracker.RemoveOnCompletion {
2✔
136
                        trackersNewlyDone = append(trackersNewlyDone, tracker)
1✔
137
                }
1✔
138
        }
139
        p.trackersActiveMutex.RUnlock()
1✔
140
        p.sortBy.Sort(trackersNewlyDone)
1✔
141
        p.sortBy.Sort(trackersActive)
1✔
142

1✔
143
        overallProgress := int64(lengthDone+len(trackersNewlyDone))*100 + activeTrackersProgress
1✔
144
        p.overallTracker.setProgress(overallProgress, maxETA)
1✔
145
        if len(trackersActive) == 0 {
2✔
146
                p.overallTracker.MarkAsDone()
1✔
147
        }
1✔
148

149
        return trackersActive, trackersNewlyDone
1✔
150
}
151

152
// extractAllTrackersInOrder extracts all trackers (both active and done) and
153
// sorts them together when SortByIndex is used. This allows maintaining a fixed
154
// order regardless of completion status. Used by the KeepTrackersTogether path.
155
func (p *Progress) extractAllTrackersInOrder() []*Tracker {
1✔
156
        // move trackers waiting in queue to the active list
1✔
157
        p.consumeQueuedTrackers()
1✔
158

1✔
159
        allTrackers, activeTrackersProgress, maxETA := p.collectActiveTrackers()
1✔
160
        p.collectDoneTrackers(&allTrackers)
1✔
161

1✔
162
        // Sort by Index (ascending or descending)
1✔
163
        if p.sortBy == SortByIndex || p.sortBy == SortByIndexDsc {
1✔
UNCOV
164
                p.sortBy.Sort(allTrackers)
×
165
        }
×
166

167
        p.updateOverallTrackerProgress(allTrackers, activeTrackersProgress, maxETA)
1✔
168

1✔
169
        return allTrackers
1✔
170
}
171

172
func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string {
1✔
173
        value, total := t.valueAndTotal()
1✔
174
        if !hint.isOverallTracker && t.IsStarted() && (total == 0 || value > total) {
2✔
175
                return p.generateTrackerStrIndeterminate(maxLen)
1✔
176
        }
1✔
177
        return p.generateTrackerStrDeterminate(value, total, maxLen)
1✔
178
}
179

180
// generateTrackerStrDeterminate generates the tracker string for the case where
181
// the Total value is known, and the progress percentage can be calculated.
182
func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLen int) string {
1✔
183
        if p.style.Renderer.TrackerDeterminate != nil {
2✔
184
                return p.style.Renderer.TrackerDeterminate(value, total, maxLen)
1✔
185
        }
1✔
186

187
        pFinishedDots, pFinishedDotsFraction := 0.0, 0.0
1✔
188
        pDotValue := float64(total) / float64(maxLen)
1✔
189
        if pDotValue > 0 {
2✔
190
                pFinishedDots = float64(value) / pDotValue
1✔
191
                pFinishedDotsFraction = pFinishedDots - float64(int(pFinishedDots))
1✔
192
        }
1✔
193
        pFinishedLen := int(math.Floor(pFinishedDots))
1✔
194

1✔
195
        var pFinished, pInProgress, pUnfinished string
1✔
196
        if pFinishedLen > 0 {
2✔
197
                pFinished = strings.Repeat(p.style.Chars.Finished, pFinishedLen)
1✔
198
        }
1✔
199
        pInProgress = p.style.Chars.Unfinished
1✔
200
        if pFinishedDotsFraction >= 0.75 {
2✔
201
                pInProgress = p.style.Chars.Finished75
1✔
202
        } else if pFinishedDotsFraction >= 0.50 {
3✔
203
                pInProgress = p.style.Chars.Finished50
1✔
204
        } else if pFinishedDotsFraction >= 0.25 {
3✔
205
                pInProgress = p.style.Chars.Finished25
1✔
206
        } else if pFinishedDotsFraction == 0 {
3✔
207
                pInProgress = ""
1✔
208
        }
1✔
209

210
        // Use strings.Builder to avoid temporary string allocation
211
        var combined strings.Builder
1✔
212
        combined.Grow(len(pFinished) + len(pInProgress))
1✔
213
        combined.WriteString(pFinished)
1✔
214
        combined.WriteString(pInProgress)
1✔
215
        pFinishedStrLen := text.StringWidthWithoutEscSequences(combined.String())
1✔
216
        if pFinishedStrLen < maxLen {
2✔
217
                pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen)
1✔
218
        }
1✔
219

220
        return p.style.Colors.Tracker.Sprintf("%s%s%s%s%s",
1✔
221
                p.style.Chars.BoxLeft, pFinished, pInProgress, pUnfinished, p.style.Chars.BoxRight,
1✔
222
        )
1✔
223
}
224

225
// generateTrackerStrIndeterminate generates the tracker string for the case where
226
// the Total value is unknown, and the progress percentage cannot be calculated.
227
func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string {
1✔
228
        if p.style.Renderer.TrackerIndeterminate != nil {
2✔
229
                return p.style.Renderer.TrackerIndeterminate(maxLen)
1✔
230
        }
1✔
231

232
        indicator := p.style.Chars.Indeterminate(maxLen)
1✔
233

1✔
234
        pUnfinished := ""
1✔
235
        if indicator.Position > 0 {
2✔
236
                pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position)
1✔
237
        }
1✔
238
        pUnfinished += indicator.Text
1✔
239
        if text.StringWidthWithoutEscSequences(pUnfinished) < maxLen {
2✔
240
                pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.StringWidthWithoutEscSequences(pUnfinished))
1✔
241
        }
1✔
242

243
        return p.style.Colors.Tracker.Sprintf("%s%s%s",
1✔
244
                p.style.Chars.BoxLeft, pUnfinished, p.style.Chars.BoxRight,
1✔
245
        )
1✔
246
}
247

248
func (p *Progress) moveCursorToTheTop(out *strings.Builder) {
1✔
249
        // Count trackers that occupy the rewindable region. In the default
1✔
250
        // scrollback mode, that's just active trackers; with KeepTrackersTogether,
1✔
251
        // done trackers are also re-rendered each tick.
1✔
252
        var numTrackersToRender int
1✔
253

1✔
254
        p.trackersActiveMutex.RLock()
1✔
255
        for _, tracker := range p.trackersActive {
2✔
256
                if !tracker.RemoveOnCompletion {
2✔
257
                        numTrackersToRender++
1✔
258
                }
1✔
259
        }
260
        p.trackersActiveMutex.RUnlock()
1✔
261

1✔
262
        if p.style.Options.KeepTrackersTogether {
1✔
UNCOV
263
                p.trackersDoneMutex.RLock()
×
264
                for _, tracker := range p.trackersDone {
×
265
                        if !tracker.RemoveOnCompletion {
×
266
                                numTrackersToRender++
×
267
                        }
×
268
                }
UNCOV
269
                p.trackersDoneMutex.RUnlock()
×
270
        }
271

272
        numLinesToMoveUp := numTrackersToRender
1✔
273
        if p.style.Visibility.TrackerOverall && p.overallTracker != nil && !p.overallTracker.IsDone() {
2✔
274
                numLinesToMoveUp++
1✔
275
        }
1✔
276
        if p.style.Visibility.Pinned {
2✔
277
                numLinesToMoveUp += p.pinnedMessageNumLines
1✔
278
        }
1✔
279
        for numLinesToMoveUp > 0 {
2✔
280
                out.WriteString(text.CursorUp.Sprint())
1✔
281
                out.WriteString(text.EraseLine.Sprint())
1✔
282
                numLinesToMoveUp--
1✔
283
        }
1✔
284
}
285

286
func (p *Progress) renderPinnedMessages(out *strings.Builder, hint renderHint) {
1✔
287
        p.pinnedMessageMutex.RLock()
1✔
288
        defer p.pinnedMessageMutex.RUnlock()
1✔
289

1✔
290
        numLines := len(p.pinnedMessages)
1✔
291
        for _, msg := range p.pinnedMessages {
2✔
292
                msg = strings.TrimSpace(msg)
1✔
293
                msg = p.style.Colors.Pinned.Sprint(msg)
1✔
294
                if hint.terminalWidth > 0 {
2✔
295
                        msg = text.Trim(msg, hint.terminalWidth)
1✔
296
                }
1✔
297
                out.WriteString(msg)
1✔
298
                out.WriteRune('\n')
1✔
299

1✔
300
                numLines += strings.Count(msg, "\n")
1✔
301
        }
302
        p.pinnedMessageNumLines = numLines
1✔
303
}
304

305
func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
306
        message := t.message()
1✔
307
        // Optimize: only process if message contains tabs or carriage returns
1✔
308
        if strings.ContainsAny(message, "\t\r") {
2✔
309
                message = strings.ReplaceAll(message, "\t", "    ")
1✔
310
                message = strings.ReplaceAll(message, "\r", "")
1✔
311
        }
1✔
312
        if p.lengthMessage > 0 {
2✔
313
                messageLen := text.StringWidthWithoutEscSequences(message)
1✔
314
                if messageLen < p.lengthMessage {
2✔
315
                        message = text.Pad(message, p.lengthMessage, ' ')
1✔
316
                } else {
2✔
317
                        message = text.Snip(message, p.lengthMessage, p.style.Options.SnipIndicator)
1✔
318
                }
1✔
319
        }
320

321
        tOut := &strings.Builder{}
1✔
322
        tOut.Grow(p.lengthProgressOverall)
1✔
323
        if hint.isOverallTracker {
2✔
324
                if !t.IsDone() {
2✔
325
                        hint := renderHint{hideValue: true, isOverallTracker: true, terminalWidth: hint.terminalWidth}
1✔
326
                        p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
1✔
327
                }
1✔
328
        } else {
1✔
329
                if t.IsDone() {
2✔
330
                        p.renderTrackerDone(tOut, t, message)
1✔
331
                } else {
2✔
332
                        hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value, terminalWidth: hint.terminalWidth}
1✔
333
                        p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
1✔
334
                }
1✔
335
        }
336

337
        outStr := tOut.String()
1✔
338
        if hint.terminalWidth > 0 {
2✔
339
                outStr = text.Trim(outStr, hint.terminalWidth)
1✔
340
        }
1✔
341
        out.WriteString(outStr)
1✔
342
        out.WriteRune('\n')
1✔
343
}
344

345
func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message string) {
1✔
346
        if !t.RemoveOnCompletion {
2✔
347
                out.WriteString(p.style.Colors.Message.Sprint(message))
1✔
348
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
349
                if !t.IsErrored() {
2✔
350
                        out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.DoneString))
1✔
351
                } else {
2✔
352
                        out.WriteString(p.style.Colors.Error.Sprint(p.style.Options.ErrorString))
1✔
353
                }
1✔
354
                p.renderTrackerStats(out, t, renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value})
1✔
355
        }
356
}
357

358
func (p *Progress) renderTrackerMessage(out *strings.Builder, t *Tracker, message string) {
1✔
359
        if !t.IsErrored() {
2✔
360
                out.WriteString(p.style.Colors.Message.Sprint(message))
1✔
361
        } else {
2✔
362
                out.WriteString(p.style.Colors.Error.Sprint(message))
1✔
363
        }
1✔
364
}
365

366
func (p *Progress) renderTrackerPercentage(out *strings.Builder, t *Tracker) {
1✔
367
        if p.style.Visibility.Percentage {
2✔
368
                var percentageStr string
1✔
369
                if t.IsIndeterminate() {
2✔
370
                        percentageStr = p.style.Options.PercentIndeterminate
1✔
371
                } else {
2✔
372
                        percentageStr = fmt.Sprintf(p.style.Options.PercentFormat, t.PercentDone())
1✔
373
                }
1✔
374
                out.WriteString(p.style.Colors.Percent.Sprint(percentageStr))
1✔
375
        }
376
}
377

378
func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, message string, trackerStr string, hint renderHint) {
1✔
379
        if hint.isOverallTracker {
2✔
380
                out.WriteString(p.style.Colors.Tracker.Sprint(trackerStr))
1✔
381
                p.renderTrackerStats(out, t, hint)
1✔
382
        } else if p.trackerPosition == PositionRight {
3✔
383
                p.renderTrackerMessage(out, t, message)
1✔
384
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
385
                p.renderTrackerPercentage(out, t)
1✔
386
                if p.style.Visibility.Tracker {
2✔
387
                        out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
1✔
388
                }
1✔
389
                p.renderTrackerStats(out, t, hint)
1✔
390
        } else {
1✔
391
                p.renderTrackerPercentage(out, t)
1✔
392
                if p.style.Visibility.Tracker {
2✔
393
                        out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
1✔
394
                }
1✔
395
                p.renderTrackerStats(out, t, hint)
1✔
396
                out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
1✔
397
                p.renderTrackerMessage(out, t, message)
1✔
398
        }
399
}
400

401
func (p *Progress) renderTrackers(lastRenderLength int) int {
1✔
402
        if p.LengthActive() == 0 {
2✔
403
                return 0
1✔
404
        }
1✔
405

406
        // Cache terminal width once per render cycle to avoid repeated mutex locks
407
        terminalWidth := p.getTerminalWidth()
1✔
408
        hint := renderHint{terminalWidth: terminalWidth}
1✔
409

1✔
410
        // buffer all output into a strings.Builder object
1✔
411
        var out strings.Builder
1✔
412
        out.Grow(lastRenderLength)
1✔
413

1✔
414
        // move up N times based on the number of active trackers
1✔
415
        if lastRenderLength > 0 {
2✔
416
                p.moveCursorToTheTop(&out)
1✔
417
        }
1✔
418

419
        if p.style.Options.KeepTrackersTogether {
2✔
420
                // flush logs above the rewindable region; moveCursorToTheTop does not count them
1✔
421
                p.renderLogs(&out)
1✔
422
                p.renderTrackersDoneAndActive(&out, hint)
1✔
423
        } else {
2✔
424
                p.renderTrackersScrollback(&out, hint)
1✔
425
        }
1✔
426

427
        // render the overall tracker
428
        if p.style.Visibility.TrackerOverall {
2✔
429
                overallHint := renderHint{isOverallTracker: true, terminalWidth: terminalWidth}
1✔
430
                p.renderTracker(&out, p.overallTracker, overallHint)
1✔
431
        }
1✔
432

433
        // write the text to the output writer
434
        p.outputWriterMutex.Lock()
1✔
435
        _, _ = p.outputWriter.Write([]byte(out.String()))
1✔
436
        p.outputWriterMutex.Unlock()
1✔
437

1✔
438
        // stop if auto stop is enabled and there are no more active trackers
1✔
439
        if p.autoStop && p.LengthActive() == 0 {
2✔
440
                p.renderContextCancelMutex.Lock()
1✔
441
                if p.renderContextCancel != nil {
2✔
442
                        p.renderContextCancel()
1✔
443
                }
1✔
444
                p.renderContextCancelMutex.Unlock()
1✔
445
        }
446

447
        return out.Len()
1✔
448
}
449

450
// renderTrackersScrollback emits newly-done trackers and logs as one-shot
451
// scrollback above the rewindable region (pinned + still-active). This is the
452
// pre-v6.7.8 layout used when StyleOptions.KeepTrackersTogether is false.
453
func (p *Progress) renderTrackersScrollback(out *strings.Builder, hint renderHint) {
1✔
454
        trackersActive, trackersNewlyDone := p.extractDoneAndActiveTrackers()
1✔
455

1✔
456
        // newly-done trackers -> scrollback
1✔
457
        for _, tracker := range trackersNewlyDone {
2✔
458
                p.renderTracker(out, tracker, hint)
1✔
459
        }
1✔
460
        p.trackersDoneMutex.Lock()
1✔
461
        p.trackersDone = append(p.trackersDone, trackersNewlyDone...)
1✔
462
        p.trackersDoneMutex.Unlock()
1✔
463

1✔
464
        // logs -> scrollback, between newly-done and the rewindable region
1✔
465
        p.renderLogs(out)
1✔
466

1✔
467
        // pinned (part of rewindable region)
1✔
468
        if len(trackersActive) > 0 && p.style.Visibility.Pinned {
2✔
469
                p.renderPinnedMessages(out, hint)
1✔
470
        }
1✔
471

472
        // active trackers (part of rewindable region)
473
        for _, tracker := range trackersActive {
2✔
474
                p.renderTracker(out, tracker, hint)
1✔
475
        }
1✔
476
        p.trackersActiveMutex.Lock()
1✔
477
        p.trackersActive = trackersActive
1✔
478
        p.trackersActiveMutex.Unlock()
1✔
479
}
480

481
func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder, hint renderHint) {
1✔
482
        // Extract all trackers (both active and done)
1✔
483
        allTrackers := p.extractAllTrackersInOrder()
1✔
484

1✔
485
        // Separate done and active trackers for sorting and state management
1✔
486
        trackersDone, trackersActive := p.separateDoneAndActiveTrackers(allTrackers)
1✔
487

1✔
488
        // Sort trackers based on sortBy setting
1✔
489
        trackersToRender := p.sortTrackersForRendering(allTrackers, trackersDone, trackersActive)
1✔
490

1✔
491
        // Render all trackers in the determined order
1✔
492
        for _, tracker := range trackersToRender {
2✔
493
                p.renderTracker(out, tracker, hint)
1✔
494
        }
1✔
495

496
        // Update internal state
497
        p.trackersDoneMutex.Lock()
1✔
498
        // Only add newly done trackers that aren't already in trackersDone
1✔
499
        existingDone := make(map[*Tracker]bool)
1✔
500
        for _, t := range p.trackersDone {
1✔
UNCOV
501
                existingDone[t] = true
×
502
        }
×
503
        for _, t := range trackersDone {
2✔
504
                if !existingDone[t] {
2✔
505
                        p.trackersDone = append(p.trackersDone, t)
1✔
506
                }
1✔
507
        }
508
        p.trackersDoneMutex.Unlock()
1✔
509

1✔
510
        p.trackersActiveMutex.Lock()
1✔
511
        p.trackersActive = trackersActive
1✔
512
        p.trackersActiveMutex.Unlock()
1✔
513

1✔
514
        // render pinned messages
1✔
515
        if len(trackersActive) > 0 && p.style.Visibility.Pinned {
2✔
516
                p.renderPinnedMessages(out, hint)
1✔
517
        }
1✔
518
}
519

520
func (p *Progress) renderLogs(out *strings.Builder) {
1✔
521
        p.logsToRenderMutex.Lock()
1✔
522
        defer p.logsToRenderMutex.Unlock()
1✔
523
        for _, log := range p.logsToRender {
2✔
524
                out.WriteString(text.EraseLine.Sprint())
1✔
525
                out.WriteString(log)
1✔
526
                out.WriteRune('\n')
1✔
527
        }
1✔
528
        p.logsToRender = nil
1✔
529
}
530

531
func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
532
        if !hint.hideValue || !hint.hideTime {
2✔
533
                var outStats strings.Builder
1✔
534
                outStats.WriteString(" [")
1✔
535

1✔
536
                if p.style.Options.SpeedPosition == PositionLeft {
2✔
537
                        p.renderTrackerStatsSpeed(&outStats, t, hint)
1✔
538
                }
1✔
539
                if !hint.hideValue {
2✔
540
                        outStats.WriteString(p.style.Colors.Value.Sprint(t.Units.Sprint(t.Value())))
1✔
541
                }
1✔
542
                if !hint.hideValue && !hint.hideTime {
2✔
543
                        outStats.WriteString(" in ")
1✔
544
                }
1✔
545
                if !hint.hideTime {
2✔
546
                        p.renderTrackerStatsTime(&outStats, t, hint)
1✔
547
                }
1✔
548
                if p.style.Options.SpeedPosition == PositionRight {
2✔
549
                        p.renderTrackerStatsSpeed(&outStats, t, hint)
1✔
550
                }
1✔
551
                outStats.WriteRune(']')
1✔
552

1✔
553
                out.WriteString(p.style.Colors.Stats.Sprint(outStats.String()))
1✔
554
        }
555
}
556

557
func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
558
        if hint.isOverallTracker && !p.style.Visibility.ETAOverall {
2✔
559
                return
1✔
560
        }
1✔
561
        if !hint.isOverallTracker && !p.style.Visibility.ETA {
2✔
562
                return
1✔
563
        }
1✔
564

565
        tpETA := p.style.Options.ETAPrecision
1✔
566
        if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA {
2✔
567
                out.WriteString("; ")
1✔
568
                out.WriteString(p.style.Options.ETAString)
1✔
569
                out.WriteString(": ")
1✔
570
                out.WriteString(p.style.Colors.Time.Sprint(eta))
1✔
571
        }
1✔
572
}
573

574
func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hint renderHint) {
1✔
575
        if hint.isOverallTracker && !p.style.Visibility.SpeedOverall {
2✔
576
                return
1✔
577
        }
1✔
578
        if !hint.isOverallTracker && !p.style.Visibility.Speed {
2✔
579
                return
1✔
580
        }
1✔
581

582
        speedPrecision := p.style.Options.SpeedPrecision
1✔
583
        if hint.isOverallTracker {
2✔
584
                speed := float64(0)
1✔
585

1✔
586
                p.trackersActiveMutex.RLock()
1✔
587
                for _, tracker := range p.trackersActive {
2✔
588
                        timeStart := tracker.timeStartValue()
1✔
589
                        if !timeStart.IsZero() {
2✔
590
                                speed += float64(tracker.Value()) / time.Since(timeStart).Round(speedPrecision).Seconds()
1✔
591
                        }
1✔
592
                }
593
                p.trackersActiveMutex.RUnlock()
1✔
594

1✔
595
                if speed > 0 {
2✔
596
                        p.renderTrackerStatsSpeedInternal(out, p.style.Options.SpeedOverallFormatter(int64(speed)))
1✔
597
                }
1✔
598
        } else {
1✔
599
                timeStart, timeStop, done := t.timeStartStopAndDone()
1✔
600
                if !timeStart.IsZero() {
2✔
601
                        var timeTaken time.Duration
1✔
602
                        if done {
2✔
603
                                timeTaken = timeStop.Sub(timeStart)
1✔
604
                        } else {
2✔
605
                                timeTaken = time.Since(timeStart)
1✔
606
                        }
1✔
607
                        if timeTakenRounded := timeTaken.Round(speedPrecision); timeTakenRounded > speedPrecision {
2✔
608
                                p.renderTrackerStatsSpeedInternal(out, t.Units.Sprint(int64(float64(t.Value())/timeTakenRounded.Seconds())))
1✔
609
                        }
1✔
610
                }
611
        }
612
}
613

614
func (p *Progress) renderTrackerStatsSpeedInternal(out *strings.Builder, speed string) {
1✔
615
        if p.style.Options.SpeedPosition == PositionRight {
2✔
616
                out.WriteString("; ")
1✔
617
        }
1✔
618
        out.WriteString(p.style.Colors.Speed.Sprint(speed))
1✔
619
        out.WriteString(p.style.Options.SpeedSuffix)
1✔
620
        if p.style.Options.SpeedPosition == PositionLeft {
2✔
621
                out.WriteString("; ")
1✔
622
        }
1✔
623
}
624

625
func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) {
1✔
626
        var td, tp time.Duration
1✔
627
        timeStart, timeStop, done := t.timeStartStopAndDone()
1✔
628
        if !timeStart.IsZero() {
2✔
629
                if done {
2✔
630
                        td = timeStop.Sub(timeStart)
1✔
631
                } else {
2✔
632
                        td = time.Since(timeStart)
1✔
633
                }
1✔
634
        }
635
        if hint.isOverallTracker {
2✔
636
                tp = p.style.Options.TimeOverallPrecision
1✔
637
        } else if done {
3✔
638
                tp = p.style.Options.TimeDonePrecision
1✔
639
        } else {
2✔
640
                tp = p.style.Options.TimeInProgressPrecision
1✔
641
        }
1✔
642
        outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp)))
1✔
643

1✔
644
        p.renderTrackerStatsETA(outStats, t, hint)
1✔
645
}
646

647
func (p *Progress) separateDoneAndActiveTrackers(allTrackers []*Tracker) ([]*Tracker, []*Tracker) {
1✔
648
        var trackersDone, trackersActive []*Tracker
1✔
649
        for _, tracker := range allTrackers {
2✔
650
                if tracker.IsDone() {
2✔
651
                        if !tracker.RemoveOnCompletion {
2✔
652
                                trackersDone = append(trackersDone, tracker)
1✔
653
                        }
1✔
654
                } else {
1✔
655
                        trackersActive = append(trackersActive, tracker)
1✔
656
                }
1✔
657
        }
658
        return trackersDone, trackersActive
1✔
659
}
660

661
func (p *Progress) sortTrackersForRendering(allTrackers []*Tracker, trackersDone []*Tracker, trackersActive []*Tracker) []*Tracker {
1✔
662
        if p.sortBy == SortByIndex || p.sortBy == SortByIndexDsc {
1✔
UNCOV
663
                // For explicit index ordering (ascending or descending), all trackers are already sorted together
×
664
                return allTrackers
×
665
        }
×
666
        // For other sort methods, sort done and active separately, then combine
667
        p.sortBy.Sort(trackersDone)
1✔
668
        p.sortBy.Sort(trackersActive)
1✔
669
        // Combine: done first, then active
1✔
670
        return append(trackersDone, trackersActive...)
1✔
671
}
672

673
func (p *Progress) updateOverallTrackerProgress(allTrackers []*Tracker, activeTrackersProgress int64, maxETA time.Duration) {
1✔
674
        doneCount := 0
1✔
675
        for _, tracker := range allTrackers {
2✔
676
                if tracker.IsDone() {
2✔
677
                        doneCount++
1✔
678
                }
1✔
679
        }
680
        overallProgress := int64(doneCount)*100 + activeTrackersProgress
1✔
681
        p.overallTracker.setProgress(overallProgress, maxETA)
1✔
682
        if len(allTrackers) > 0 && doneCount == len(allTrackers) {
1✔
UNCOV
683
                p.overallTracker.MarkAsDone()
×
UNCOV
684
        }
×
685
}
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