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

jedib0t / go-pretty / 26191557037

20 May 2026 09:39PM UTC coverage: 99.636% (-0.4%) from 100.0%
26191557037

Pull #406

github

jedib0t
progress: KeepTrackersTogether option; restore pre-v6.7.8 layout as default

Makes v6.7.8's merged-tracker-list behavior opt-in via
StyleOptions.KeepTrackersTogether. Default restores the pre-v6.7.8
layout: done trackers exit the rewindable region on completion, logs
sit between newly-done and active. SortByIndex / SortByIndexDsc need
the option to reorder trackers after they complete.
Pull Request #406: progress: fix speed decay on done trackers and log overwrite; fixes #405

89 of 95 new or added lines in 2 files covered. (93.68%)

12 existing lines in 1 file now uncovered.

4931 of 4949 relevant lines covered (99.64%)

1.2 hits per line

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

96.72
/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✔
UNCOV
77
                if !tracker.RemoveOnCompletion {
×
UNCOV
78
                        *allTrackers = append(*allTrackers, tracker)
×
UNCOV
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
        p.overallTracker.value = int64(lengthDone+len(trackersNewlyDone)) * 100
1✔
144
        p.overallTracker.value += activeTrackersProgress
1✔
145
        p.overallTracker.minETA = maxETA
1✔
146
        if len(trackersActive) == 0 {
2✔
147
                p.overallTracker.MarkAsDone()
1✔
148
        }
1✔
149

150
        return trackersActive, trackersNewlyDone
1✔
151
}
152

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

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

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

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

1✔
170
        return allTrackers
1✔
171
}
172

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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