• 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

99.06
/progress/progress.go
1
package progress
2

3
import (
4
        "context"
5
        "fmt"
6
        "io"
7
        "os"
8
        "sync"
9
        "time"
10

11
        "github.com/jedib0t/go-pretty/v6/text"
12
        "golang.org/x/term"
13
)
14

15
var (
16
        // DefaultLengthTracker defines a sane value for a Tracker's length.
17
        DefaultLengthTracker = 20
18

19
        // DefaultUpdateFrequency defines a sane value for the frequency with which
20
        // all the Tracker's get updated on the screen.
21
        DefaultUpdateFrequency = time.Millisecond * 250
22
)
23

24
// Progress helps track progress for one or more tasks.
25
type Progress struct {
26
        autoStop                 bool
27
        lengthMessage            int
28
        lengthProgress           int
29
        lengthProgressOverall    int
30
        lengthTracker            int
31
        logsToRender             []string
32
        logsToRenderMutex        sync.RWMutex
33
        numTrackersExpected      int64
34
        outputWriter             io.Writer
35
        outputWriterMutex        sync.RWMutex
36
        overallTracker           *Tracker
37
        overallTrackerMutex      sync.RWMutex
38
        pinnedMessages           []string
39
        pinnedMessageMutex       sync.RWMutex
40
        pinnedMessageNumLines    int
41
        renderContext            context.Context
42
        renderContextCancel      context.CancelFunc
43
        renderContextCancelMutex sync.Mutex
44
        renderInProgress         bool
45
        renderInProgressMutex    sync.RWMutex
46
        sortBy                   SortBy
47
        style                    *Style
48
        terminalWidth            int
49
        terminalWidthMutex       sync.RWMutex
50
        terminalWidthOverride    int
51
        trackerPosition          Position
52
        trackersActive           []*Tracker
53
        trackersActiveMutex      sync.RWMutex
54
        trackersDone             []*Tracker
55
        trackersDoneMutex        sync.RWMutex
56
        trackersInQueue          []*Tracker
57
        trackersInQueueMutex     sync.RWMutex
58
        updateFrequency          time.Duration
59
}
60

61
// Position defines the position of the Tracker with respect to the Tracker's
62
// Message.
63
type Position int
64

65
const (
66
        // PositionLeft will make the Tracker be displayed first before the Message.
67
        PositionLeft Position = iota
68

69
        // PositionRight will make the Tracker be displayed after the Message.
70
        PositionRight
71
)
72

73
// AppendTracker appends a single Tracker for tracking. The Tracker gets added
74
// to a queue, which gets picked up by the Render logic in the next rendering
75
// cycle.
76
func (p *Progress) AppendTracker(t *Tracker) {
1✔
77
        if !t.DeferStart {
2✔
78
                t.start()
1✔
79
        }
1✔
80
        p.overallTrackerMutex.Lock()
1✔
81
        defer p.overallTrackerMutex.Unlock()
1✔
82

1✔
83
        if p.overallTracker == nil {
2✔
84
                p.overallTracker = &Tracker{Total: 1}
1✔
85
                if p.numTrackersExpected > 0 {
2✔
86
                        p.overallTracker.Total = p.numTrackersExpected * 100
1✔
87
                }
1✔
88
                p.overallTracker.start()
1✔
89
        }
90

91
        // append the tracker to the "in-queue" list
92
        p.trackersInQueueMutex.Lock()
1✔
93
        p.trackersInQueue = append(p.trackersInQueue, t)
1✔
94
        p.trackersInQueueMutex.Unlock()
1✔
95

1✔
96
        // update the expected total progress since we are appending a new tracker
1✔
97
        p.overallTracker.UpdateTotal(int64(p.Length()) * 100)
1✔
98
}
99

100
// AppendTrackers appends one or more Trackers for tracking.
101
func (p *Progress) AppendTrackers(trackers []*Tracker) {
1✔
102
        for _, tracker := range trackers {
2✔
103
                p.AppendTracker(tracker)
1✔
104
        }
1✔
105
}
106

107
// IsRenderInProgress returns true if a call to Render() was made, and is still
108
// in progress and has not ended yet.
109
func (p *Progress) IsRenderInProgress() bool {
1✔
110
        p.renderInProgressMutex.RLock()
1✔
111
        defer p.renderInProgressMutex.RUnlock()
1✔
112

1✔
113
        return p.renderInProgress
1✔
114
}
1✔
115

116
// Length returns the number of Trackers tracked overall.
117
func (p *Progress) Length() int {
1✔
118
        p.trackersActiveMutex.RLock()
1✔
119
        p.trackersDoneMutex.RLock()
1✔
120
        p.trackersInQueueMutex.RLock()
1✔
121
        out := len(p.trackersInQueue) + len(p.trackersActive) + len(p.trackersDone)
1✔
122
        p.trackersInQueueMutex.RUnlock()
1✔
123
        p.trackersDoneMutex.RUnlock()
1✔
124
        p.trackersActiveMutex.RUnlock()
1✔
125

1✔
126
        return out
1✔
127
}
1✔
128

129
// LengthActive returns the number of Trackers actively tracked (not done yet).
130
func (p *Progress) LengthActive() int {
1✔
131
        p.trackersActiveMutex.RLock()
1✔
132
        p.trackersInQueueMutex.RLock()
1✔
133
        out := len(p.trackersInQueue) + len(p.trackersActive)
1✔
134
        p.trackersInQueueMutex.RUnlock()
1✔
135
        p.trackersActiveMutex.RUnlock()
1✔
136

1✔
137
        return out
1✔
138
}
1✔
139

140
// LengthDone returns the number of Trackers that are done tracking.
141
func (p *Progress) LengthDone() int {
1✔
142
        p.trackersDoneMutex.RLock()
1✔
143
        out := len(p.trackersDone)
1✔
144
        p.trackersDoneMutex.RUnlock()
1✔
145

1✔
146
        return out
1✔
147
}
1✔
148

149
// LengthInQueue returns the number of Trackers in queue to be actively tracked
150
// (not tracking yet).
151
func (p *Progress) LengthInQueue() int {
1✔
152
        p.trackersInQueueMutex.RLock()
1✔
153
        out := len(p.trackersInQueue)
1✔
154
        p.trackersInQueueMutex.RUnlock()
1✔
155

1✔
156
        return out
1✔
157
}
1✔
158

159
// Log appends a log to display above the active progress bars during the next
160
// refresh.
161
func (p *Progress) Log(msg string, a ...interface{}) {
1✔
162
        if len(a) > 0 {
2✔
163
                msg = fmt.Sprintf(msg, a...)
1✔
164
        }
1✔
165
        p.logsToRenderMutex.Lock()
1✔
166
        p.logsToRender = append(p.logsToRender, msg)
1✔
167
        p.logsToRenderMutex.Unlock()
1✔
168
}
169

170
// SetAutoStop toggles the auto-stop functionality. Auto-stop set to true would
171
// mean that the Render() function will automatically stop once all currently
172
// active Trackers reach their final states. When set to false, the client code
173
// will have to call Progress.Stop() to stop the Render() logic. Default: false.
174
func (p *Progress) SetAutoStop(autoStop bool) {
1✔
175
        p.autoStop = autoStop
1✔
176
}
1✔
177

178
// SetMessageLength sets the (printed) length of the tracker message. Any
179
// message longer the specified length will be snipped. Any message shorter than
180
// the specified width will be padded with spaces.
181
func (p *Progress) SetMessageLength(length int) {
1✔
182
        p.lengthMessage = length
1✔
183
}
1✔
184

185
// SetMessageWidth sets the (printed) length of the tracker message. Any message
186
// longer the specified width will be snipped. Any message shorter than the
187
// specified width will be padded with spaces.
188
// Deprecated: in favor of SetMessageLength(length)
189
func (p *Progress) SetMessageWidth(width int) {
1✔
190
        p.lengthMessage = width
1✔
191
}
1✔
192

193
// SetNumTrackersExpected sets the expected number of trackers to be tracked.
194
// This helps calculate the overall progress with better accuracy.
195
func (p *Progress) SetNumTrackersExpected(numTrackers int) {
1✔
196
        p.numTrackersExpected = int64(numTrackers)
1✔
197
}
1✔
198

199
// SetOutputWriter redirects the output of Render to an io.writer object like
200
// os.Stdout or os.Stderr or a file. Warning: redirecting the output to a file
201
// may not work well as the Render() logic moves the cursor around a lot.
202
func (p *Progress) SetOutputWriter(writer io.Writer) {
1✔
203
        p.outputWriterMutex.Lock()
1✔
204
        p.outputWriter = writer
1✔
205
        p.outputWriterMutex.Unlock()
1✔
206
}
1✔
207

208
// SetPinnedMessages sets message(s) pinned above all the trackers of the
209
// progress bar. This method can be used to overwrite all the pinned messages.
210
// Call this function without arguments to "clear" the pinned messages.
211
func (p *Progress) SetPinnedMessages(messages ...string) {
1✔
212
        p.pinnedMessageMutex.Lock()
1✔
213
        defer p.pinnedMessageMutex.Unlock()
1✔
214

1✔
215
        p.pinnedMessages = messages
1✔
216
}
1✔
217

218
// SetSortBy defines the sorting mechanism to use to sort the Active Trackers
219
// before rendering. Default: no-sorting == sort-by-insertion-order.
220
func (p *Progress) SetSortBy(sortBy SortBy) {
1✔
221
        p.sortBy = sortBy
1✔
222
}
1✔
223

224
// SetStyle sets the Style to use for rendering.
225
func (p *Progress) SetStyle(style Style) {
1✔
226
        p.style = &style
1✔
227
}
1✔
228

229
// SetTerminalWidth sets up a sticky terminal width and prevents the Progress
230
// Writer from polling for the real width during render.
231
func (p *Progress) SetTerminalWidth(width int) {
1✔
232
        p.terminalWidthOverride = width
1✔
233
}
1✔
234

235
// SetTrackerLength sets the text-length of all the Trackers.
236
func (p *Progress) SetTrackerLength(length int) {
1✔
237
        p.lengthTracker = length
1✔
238
}
1✔
239

240
// SetTrackerPosition sets the position of the tracker with respect to the
241
// Tracker message text.
242
func (p *Progress) SetTrackerPosition(position Position) {
1✔
243
        p.trackerPosition = position
1✔
244
}
1✔
245

246
// SetUpdateFrequency sets the update frequency while rendering the trackers.
247
// the lower the value, the more frequently the Trackers get refreshed. A
248
// sane value would be 250ms.
249
func (p *Progress) SetUpdateFrequency(frequency time.Duration) {
1✔
250
        p.updateFrequency = frequency
1✔
251
}
1✔
252

253
// ShowETA toggles showing the ETA for all individual trackers.
254
// Deprecated: in favor of Style().Visibility.ETA
255
func (p *Progress) ShowETA(show bool) {
1✔
256
        p.Style().Visibility.ETA = show
1✔
257
}
1✔
258

259
// ShowPercentage toggles showing the Percent complete for each Tracker.
260
// Deprecated: in favor of Style().Visibility.Percentage
261
func (p *Progress) ShowPercentage(show bool) {
1✔
262
        p.Style().Visibility.Percentage = show
1✔
263
}
1✔
264

265
// ShowOverallTracker toggles showing the Overall progress tracker with an ETA.
266
// Deprecated: in favor of Style().Visibility.TrackerOverall
267
func (p *Progress) ShowOverallTracker(show bool) {
1✔
268
        p.Style().Visibility.TrackerOverall = show
1✔
269
}
1✔
270

271
// ShowTime toggles showing the Time taken by each Tracker.
272
// Deprecated: in favor of Style().Visibility.Time
273
func (p *Progress) ShowTime(show bool) {
1✔
274
        p.Style().Visibility.Time = show
1✔
275
}
1✔
276

277
// ShowTracker toggles showing the Tracker (the progress bar).
278
// Deprecated: in favor of Style().Visibility.Tracker
279
func (p *Progress) ShowTracker(show bool) {
1✔
280
        p.Style().Visibility.Tracker = show
1✔
281
}
1✔
282

283
// ShowValue toggles showing the actual Value of the Tracker.
284
// Deprecated: in favor of Style().Visibility.Value
285
func (p *Progress) ShowValue(show bool) {
1✔
286
        p.Style().Visibility.Value = show
1✔
287
}
1✔
288

289
// Stop stops the Render() logic that is in progress.
290
func (p *Progress) Stop() {
1✔
291
        p.renderContextCancelMutex.Lock()
1✔
292
        defer p.renderContextCancelMutex.Unlock()
1✔
293

1✔
294
        if p.renderContextCancel != nil {
2✔
295
                p.renderContextCancel()
1✔
296
        }
1✔
297
}
298

299
// Style returns the current Style.
300
func (p *Progress) Style() *Style {
1✔
301
        if p.style == nil {
2✔
302
                tempStyle := StyleDefault
1✔
303
                p.style = &tempStyle
1✔
304
        }
1✔
305
        return p.style
1✔
306
}
307

308
func (p *Progress) getTerminalWidth() int {
1✔
309
        p.terminalWidthMutex.RLock()
1✔
310
        defer p.terminalWidthMutex.RUnlock()
1✔
311

1✔
312
        if p.terminalWidthOverride > 0 {
2✔
313
                return p.terminalWidthOverride
1✔
314
        }
1✔
315
        return p.terminalWidth
1✔
316
}
317

318
func (p *Progress) initForRender() {
1✔
319
        // reset the signals
1✔
320
        p.renderContextCancelMutex.Lock()
1✔
321
        p.renderContext, p.renderContextCancel = context.WithCancel(context.Background())
1✔
322
        p.renderContextCancelMutex.Unlock()
1✔
323

1✔
324
        // pick a default style
1✔
325
        p.Style()
1✔
326
        if p.style.Options.SpeedOverallFormatter == nil {
2✔
327
                p.style.Options.SpeedOverallFormatter = FormatNumber
1✔
328
        }
1✔
329

330
        // pick default lengths if no valid ones set
331
        if p.lengthTracker <= 0 {
2✔
332
                p.lengthTracker = DefaultLengthTracker
1✔
333
        }
1✔
334

335
        // calculate length of the actual progress bar by discounting the left/right
336
        // border/box chars
337
        p.lengthProgress = p.lengthTracker -
1✔
338
                text.StringWidthWithoutEscSequences(p.style.Chars.BoxLeft) -
1✔
339
                text.StringWidthWithoutEscSequences(p.style.Chars.BoxRight)
1✔
340
        if p.lengthProgress < 1 { // wide box chars can leave no room for the bar
1✔
UNCOV
341
                p.lengthProgress = 1
×
UNCOV
342
        }
×
343
        p.lengthProgressOverall = p.lengthMessage +
1✔
344
                text.StringWidthWithoutEscSequences(p.style.Options.Separator) +
1✔
345
                p.lengthProgress + 1
1✔
346
        if p.style.Visibility.Percentage {
2✔
347
                p.lengthProgressOverall += text.StringWidthWithoutEscSequences(
1✔
348
                        fmt.Sprintf(p.style.Options.PercentFormat, 0.0),
1✔
349
                )
1✔
350
        }
1✔
351

352
        // if not output write has been set, output to STDOUT
353
        p.outputWriterMutex.RLock()
1✔
354
        if p.outputWriter == nil {
2✔
355
                p.outputWriter = os.Stdout
1✔
356
        }
1✔
357
        outputWriter := p.outputWriter
1✔
358
        p.outputWriterMutex.RUnlock()
1✔
359

1✔
360
        // pick a sane update frequency if none set
1✔
361
        if p.updateFrequency <= 0 {
2✔
362
                p.updateFrequency = DefaultUpdateFrequency
1✔
363
        }
1✔
364

365
        if outputWriter == os.Stdout {
2✔
366
                // get the current terminal size for preventing roll-overs, and do this in a
1✔
367
                // background loop until end of render. This only works if the output writer is STDOUT.
1✔
368
                go p.watchTerminalSize() // needs p.updateFrequency
1✔
369
        }
1✔
370
}
371

372
func (p *Progress) updateTerminalSize() {
1✔
373
        p.terminalWidthMutex.Lock()
1✔
374
        defer p.terminalWidthMutex.Unlock()
1✔
375

1✔
376
        p.terminalWidth, _, _ = term.GetSize(int(os.Stdout.Fd()))
1✔
377
}
1✔
378

379
func (p *Progress) watchTerminalSize() {
1✔
380
        // once
1✔
381
        p.updateTerminalSize()
1✔
382
        // until end of time
1✔
383
        ticker := time.NewTicker(time.Second / 10)
1✔
384
        defer ticker.Stop()
1✔
385
        for {
2✔
386
                select {
1✔
387
                case <-ticker.C:
1✔
388
                        p.updateTerminalSize()
1✔
389
                case <-p.renderContext.Done():
1✔
390
                        return
1✔
391
                }
392
        }
393
}
394

395
// renderHint has hints for the Render*() logic
396
type renderHint struct {
397
        hideTime         bool // hide the time
398
        hideValue        bool // hide the value
399
        isOverallTracker bool // is the Overall Progress tracker
400
        terminalWidth    int  // cached terminal width for this render cycle
401
}
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