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

jedib0t / go-pretty / 19111272497

05 Nov 2025 05:50PM UTC coverage: 99.773% (-0.1%) from 99.874%
19111272497

Pull #375

github

web-flow
Merge branch 'main' into stdout
Pull Request #375: Only adjust terminal size when the output writer is stdout

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

4 existing lines in 2 files now uncovered.

3961 of 3970 relevant lines covered (99.77%)

1.22 hits per line

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

99.0
/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
        overallTracker           *Tracker
36
        overallTrackerMutex      sync.RWMutex
37
        pinnedMessages           []string
38
        pinnedMessageMutex       sync.RWMutex
39
        pinnedMessageNumLines    int
40
        renderContext            context.Context
41
        renderContextCancel      context.CancelFunc
42
        renderContextCancelMutex sync.Mutex
43
        renderInProgress         bool
44
        renderInProgressMutex    sync.RWMutex
45
        sortBy                   SortBy
46
        style                    *Style
47
        terminalWidth            int
48
        terminalWidthMutex       sync.RWMutex
49
        terminalWidthOverride    int
50
        trackerPosition          Position
51
        trackersActive           []*Tracker
52
        trackersActiveMutex      sync.RWMutex
53
        trackersDone             []*Tracker
54
        trackersDoneMutex        sync.RWMutex
55
        trackersInQueue          []*Tracker
56
        trackersInQueueMutex     sync.RWMutex
57
        updateFrequency          time.Duration
58
}
59

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

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

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

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

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

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

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

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

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

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

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

1✔
125
        return out
1✔
126
}
1✔
127

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

1✔
136
        return out
1✔
137
}
1✔
138

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

1✔
145
        return out
1✔
146
}
1✔
147

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

1✔
155
        return out
1✔
156
}
1✔
157

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

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

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

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

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

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

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

1✔
212
        p.pinnedMessages = messages
1✔
213
}
1✔
214

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
291
        if p.renderContextCancel != nil {
2✔
292
                p.renderContextCancel()
1✔
293
        }
1✔
294
}
295

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

305
func (p *Progress) getTerminalWidth() int {
1✔
306
        p.terminalWidthMutex.RLock()
1✔
307
        defer p.terminalWidthMutex.RUnlock()
1✔
308

1✔
309
        if p.terminalWidthOverride > 0 {
2✔
310
                return p.terminalWidthOverride
1✔
311
        }
1✔
312
        return p.terminalWidth
1✔
313
}
314

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

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

327
        // pick default lengths if no valid ones set
328
        if p.lengthTracker <= 0 {
2✔
329
                p.lengthTracker = DefaultLengthTracker
1✔
330
        }
1✔
331

332
        // calculate length of the actual progress bar by discounting the left/right
333
        // border/box chars
334
        p.lengthProgress = p.lengthTracker -
1✔
335
                text.StringWidthWithoutEscSequences(p.style.Chars.BoxLeft) -
1✔
336
                text.StringWidthWithoutEscSequences(p.style.Chars.BoxRight)
1✔
337
        p.lengthProgressOverall = p.lengthMessage +
1✔
338
                text.StringWidthWithoutEscSequences(p.style.Options.Separator) +
1✔
339
                p.lengthProgress + 1
1✔
340
        if p.style.Visibility.Percentage {
2✔
341
                p.lengthProgressOverall += text.StringWidthWithoutEscSequences(
1✔
342
                        fmt.Sprintf(p.style.Options.PercentFormat, 0.0),
1✔
343
                )
1✔
344
        }
1✔
345

346
        // if not output write has been set, output to STDOUT
347
        if p.outputWriter == nil {
2✔
348
                p.outputWriter = os.Stdout
1✔
349
        }
1✔
350

351
        // pick a sane update frequency if none set
352
        if p.updateFrequency <= 0 {
2✔
353
                p.updateFrequency = DefaultUpdateFrequency
1✔
354
        }
1✔
355

356
        if p.outputWriter == os.Stdout {
2✔
357
                // get the current terminal size for preventing roll-overs, and do this in a
1✔
358
                // background loop until end of render. This only works if the output writer is STDOUT.
1✔
359
                go p.watchTerminalSize() // needs p.updateFrequency
1✔
360
        }
1✔
361
}
362

363
func (p *Progress) updateTerminalSize() {
1✔
364
        p.terminalWidthMutex.Lock()
1✔
365
        defer p.terminalWidthMutex.Unlock()
1✔
366

1✔
367
        p.terminalWidth, _, _ = term.GetSize(int(os.Stdout.Fd()))
1✔
368
}
1✔
369

370
func (p *Progress) watchTerminalSize() {
1✔
371
        // once
1✔
372
        p.updateTerminalSize()
1✔
373
        // until end of time
1✔
374
        ticker := time.NewTicker(time.Second / 10)
1✔
375
        for {
2✔
376
                select {
1✔
377
                case <-ticker.C:
1✔
378
                        p.updateTerminalSize()
1✔
UNCOV
379
                case <-p.renderContext.Done():
×
UNCOV
380
                        return
×
381
                }
382
        }
383
}
384

385
// renderHint has hints for the Render*() logic
386
type renderHint struct {
387
        hideTime         bool // hide the time
388
        hideValue        bool // hide the value
389
        isOverallTracker bool // is the Overall Progress tracker
390
}
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

© 2025 Coveralls, Inc