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
13
14 func (p *Progress) Render() {
15 if p.beginRender() {
16 p.initForRender()
17
18 lastRenderLength := 0
19 ticker := time.NewTicker(p.updateFrequency)
20 defer ticker.Stop()
21 for {
22 select {
23 case <-ticker.C:
24 lastRenderLength = p.renderTrackers(lastRenderLength)
25 case <-p.done:
26
27
28 p.renderTrackers(lastRenderLength)
29 p.endRender()
30 return
31 }
32 }
33 }
34 }
35
36 func (p *Progress) beginRender() bool {
37 p.renderInProgressMutex.Lock()
38 defer p.renderInProgressMutex.Unlock()
39
40 if p.renderInProgress {
41 return false
42 }
43 p.renderInProgress = true
44 return true
45 }
46
47 func (p *Progress) consumeQueuedTrackers() {
48 if p.LengthInQueue() > 0 {
49 p.trackersActiveMutex.Lock()
50 p.trackersInQueueMutex.Lock()
51 p.trackersActive = append(p.trackersActive, p.trackersInQueue...)
52 p.trackersInQueue = make([]*Tracker, 0)
53 p.trackersInQueueMutex.Unlock()
54 p.trackersActiveMutex.Unlock()
55 }
56 }
57
58 func (p *Progress) endRender() {
59 p.renderInProgressMutex.Lock()
60 defer p.renderInProgressMutex.Unlock()
61
62 p.renderInProgress = false
63 }
64
65 func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
66
67 p.consumeQueuedTrackers()
68
69
70 var trackersActive, trackersDone []*Tracker
71 var activeTrackersProgress int64
72 p.trackersActiveMutex.RLock()
73 for _, tracker := range p.trackersActive {
74 if !tracker.IsDone() {
75 trackersActive = append(trackersActive, tracker)
76 activeTrackersProgress += int64(tracker.PercentDone())
77 } else {
78 trackersDone = append(trackersDone, tracker)
79 }
80 }
81 p.trackersActiveMutex.RUnlock()
82 p.sortBy.Sort(trackersDone)
83 p.sortBy.Sort(trackersActive)
84
85
86 p.overallTracker.value = int64(p.LengthDone()+len(trackersDone)) * 100
87 p.overallTracker.value += activeTrackersProgress
88 if len(trackersActive) == 0 {
89 p.overallTracker.MarkAsDone()
90 }
91 return trackersActive, trackersDone
92 }
93
94 func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string {
95 value, total := t.valueAndTotal()
96 if !hint.isOverallTracker && (total == 0 || value > total) {
97 return p.generateTrackerStrIndeterminate(maxLen)
98 }
99 return p.generateTrackerStrDeterminate(value, total, maxLen)
100 }
101
102
103
104 func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLen int) string {
105 pFinishedDots, pFinishedDotsFraction := 0.0, 0.0
106 pDotValue := float64(total) / float64(maxLen)
107 if pDotValue > 0 {
108 pFinishedDots = float64(value) / pDotValue
109 pFinishedDotsFraction = pFinishedDots - float64(int(pFinishedDots))
110 }
111 pFinishedLen := int(math.Floor(pFinishedDots))
112
113 var pFinished, pInProgress, pUnfinished string
114 if pFinishedLen > 0 {
115 pFinished = strings.Repeat(p.style.Chars.Finished, pFinishedLen)
116 }
117 pInProgress = p.style.Chars.Unfinished
118 if pFinishedDotsFraction >= 0.75 {
119 pInProgress = p.style.Chars.Finished75
120 } else if pFinishedDotsFraction >= 0.50 {
121 pInProgress = p.style.Chars.Finished50
122 } else if pFinishedDotsFraction >= 0.25 {
123 pInProgress = p.style.Chars.Finished25
124 } else if pFinishedDotsFraction == 0 {
125 pInProgress = ""
126 }
127 pFinishedStrLen := text.RuneWidthWithoutEscSequences(pFinished + pInProgress)
128 if pFinishedStrLen < maxLen {
129 pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen)
130 }
131
132 return p.style.Colors.Tracker.Sprintf("%s%s%s%s%s",
133 p.style.Chars.BoxLeft, pFinished, pInProgress, pUnfinished, p.style.Chars.BoxRight,
134 )
135 }
136
137
138
139 func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string {
140 indicator := p.style.Chars.Indeterminate(maxLen)
141
142 pUnfinished := ""
143 if indicator.Position > 0 {
144 pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position)
145 }
146 pUnfinished += indicator.Text
147 if text.RuneWidthWithoutEscSequences(pUnfinished) < maxLen {
148 pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.RuneWidthWithoutEscSequences(pUnfinished))
149 }
150
151 return p.style.Colors.Tracker.Sprintf("%s%s%s",
152 p.style.Chars.BoxLeft, string(pUnfinished), p.style.Chars.BoxRight,
153 )
154 }
155
156 func (p *Progress) moveCursorToTheTop(out *strings.Builder) {
157 numLinesToMoveUp := len(p.trackersActive)
158 if p.style.Visibility.TrackerOverall && p.overallTracker != nil && !p.overallTracker.IsDone() {
159 numLinesToMoveUp++
160 }
161 if numLinesToMoveUp > 0 {
162 out.WriteString(text.CursorUp.Sprintn(numLinesToMoveUp))
163 }
164 }
165
166 func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) {
167 message := t.message()
168 if strings.Contains(message, "\t") {
169 message = strings.Replace(message, "\t", " ", -1)
170 }
171 if strings.Contains(message, "\r") {
172 message = strings.Replace(message, "\r", "", -1)
173 }
174 if p.messageWidth > 0 {
175 messageLen := text.RuneWidthWithoutEscSequences(message)
176 if messageLen < p.messageWidth {
177 message = text.Pad(message, p.messageWidth, ' ')
178 } else {
179 message = text.Snip(message, p.messageWidth, p.style.Options.SnipIndicator)
180 }
181 }
182
183 out.WriteString(text.EraseLine.Sprint())
184 if hint.isOverallTracker {
185 if !t.IsDone() {
186 hint := renderHint{hideValue: true, isOverallTracker: true}
187 p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
188 }
189 } else {
190 if t.IsDone() {
191 p.renderTrackerDone(out, t, message)
192 } else {
193 hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value}
194 p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
195 }
196 }
197 }
198
199 func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message string) {
200 out.WriteString(p.style.Colors.Message.Sprint(message))
201 out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
202 if !t.IsErrored() {
203 out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.DoneString))
204 } else {
205 out.WriteString(p.style.Colors.Error.Sprint(p.style.Options.ErrorString))
206 }
207 p.renderTrackerStats(out, t, renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value})
208 out.WriteRune('\n')
209 }
210
211 func (p *Progress) renderTrackerMessage(out *strings.Builder, t *Tracker, message string) {
212 if !t.IsErrored() {
213 out.WriteString(p.style.Colors.Message.Sprint(message))
214 } else {
215 out.WriteString(p.style.Colors.Error.Sprint(message))
216 }
217 }
218
219 func (p *Progress) renderTrackerPercentage(out *strings.Builder, t *Tracker) {
220 if p.style.Visibility.Percentage {
221 var percentageStr string
222 if t.IsIndeterminate() {
223 percentageStr = p.style.Options.PercentIndeterminate
224 } else {
225 percentageStr = fmt.Sprintf(p.style.Options.PercentFormat, t.PercentDone())
226 }
227 out.WriteString(p.style.Colors.Percent.Sprint(percentageStr))
228 }
229 }
230
231 func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, message string, trackerStr string, hint renderHint) {
232 if hint.isOverallTracker {
233 out.WriteString(p.style.Colors.Tracker.Sprint(trackerStr))
234 p.renderTrackerStats(out, t, hint)
235 out.WriteRune('\n')
236 } else if p.trackerPosition == PositionRight {
237 p.renderTrackerMessage(out, t, message)
238 out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
239 p.renderTrackerPercentage(out, t)
240 if p.style.Visibility.Tracker {
241 out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
242 }
243 p.renderTrackerStats(out, t, hint)
244 out.WriteRune('\n')
245 } else {
246 p.renderTrackerPercentage(out, t)
247 if p.style.Visibility.Tracker {
248 out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
249 }
250 p.renderTrackerStats(out, t, hint)
251 out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
252 p.renderTrackerMessage(out, t, message)
253 out.WriteRune('\n')
254 }
255 }
256
257 func (p *Progress) renderTrackers(lastRenderLength int) int {
258 if p.LengthActive() == 0 {
259 return 0
260 }
261
262
263 var out strings.Builder
264 out.Grow(lastRenderLength)
265
266
267 if lastRenderLength > 0 {
268 p.moveCursorToTheTop(&out)
269 }
270
271
272 p.renderTrackersDoneAndActive(&out)
273
274
275 if p.style.Visibility.TrackerOverall {
276 p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true})
277 }
278
279
280 _, _ = p.outputWriter.Write([]byte(out.String()))
281
282
283 if p.autoStop && p.LengthActive() == 0 {
284 p.done <- true
285 }
286
287 return out.Len()
288 }
289
290 func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder) {
291
292 trackersActive, trackersDone := p.extractDoneAndActiveTrackers()
293
294
295 for _, tracker := range trackersDone {
296 p.renderTracker(out, tracker, renderHint{})
297 }
298 p.trackersDoneMutex.Lock()
299 p.trackersDone = append(p.trackersDone, trackersDone...)
300 p.trackersDoneMutex.Unlock()
301
302
303 p.logsToRenderMutex.Lock()
304 for _, log := range p.logsToRender {
305 out.WriteString(text.EraseLine.Sprint())
306 out.WriteString(log)
307 out.WriteRune('\n')
308 }
309 p.logsToRender = nil
310 p.logsToRenderMutex.Unlock()
311
312
313 for _, tracker := range trackersActive {
314 p.renderTracker(out, tracker, renderHint{})
315 }
316 p.trackersActiveMutex.Lock()
317 p.trackersActive = trackersActive
318 p.trackersActiveMutex.Unlock()
319 }
320
321 func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) {
322 if !hint.hideValue || !hint.hideTime {
323 var outStats strings.Builder
324 outStats.WriteString(" [")
325
326 if p.style.Options.SpeedPosition == PositionLeft {
327 p.renderTrackerStatsSpeed(&outStats, t, hint)
328 }
329 if !hint.hideValue {
330 outStats.WriteString(p.style.Colors.Value.Sprint(t.Units.Sprint(t.Value())))
331 }
332 if !hint.hideValue && !hint.hideTime {
333 outStats.WriteString(" in ")
334 }
335 if !hint.hideTime {
336 p.renderTrackerStatsTime(&outStats, t, hint)
337 }
338 if p.style.Options.SpeedPosition == PositionRight {
339 p.renderTrackerStatsSpeed(&outStats, t, hint)
340 }
341 outStats.WriteRune(']')
342
343 out.WriteString(p.style.Colors.Stats.Sprint(outStats.String()))
344 }
345 }
346
347 func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hint renderHint) {
348 if hint.isOverallTracker && !p.style.Visibility.SpeedOverall {
349 return
350 }
351 if !hint.isOverallTracker && !p.style.Visibility.Speed {
352 return
353 }
354
355 speedPrecision := p.style.Options.SpeedPrecision
356 writeSpeed := func(speed string) {
357 if p.style.Options.SpeedPosition == PositionRight {
358 out.WriteString("; ")
359 }
360 out.WriteString(p.style.Colors.Speed.Sprint(speed))
361 out.WriteString(p.style.Options.SpeedSuffix)
362 if p.style.Options.SpeedPosition == PositionLeft {
363 out.WriteString("; ")
364 }
365 }
366
367 if hint.isOverallTracker {
368 speed := float64(0)
369
370 p.trackersActiveMutex.RLock()
371 for _, tracker := range p.trackersActive {
372 speed += float64(tracker.Value()) / time.Since(tracker.timeStart).Round(speedPrecision).Seconds()
373 }
374 p.trackersActiveMutex.RUnlock()
375
376 if speed > 0 {
377 writeSpeed(p.style.Options.SpeedOverallFormatter(int64(speed)))
378 }
379 } else {
380 timeTaken := time.Since(t.timeStart)
381 if timeTakenRounded := timeTaken.Round(speedPrecision); timeTakenRounded > speedPrecision {
382 writeSpeed(t.Units.Sprint(int64(float64(t.Value()) / timeTakenRounded.Seconds())))
383 }
384 }
385 }
386
387 func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) {
388 var td, tp time.Duration
389 if t.IsDone() {
390 td = t.timeStop.Sub(t.timeStart)
391 } else {
392 td = time.Since(t.timeStart)
393 }
394 if hint.isOverallTracker {
395 tp = p.style.Options.TimeOverallPrecision
396 } else if t.IsDone() {
397 tp = p.style.Options.TimeDonePrecision
398 } else {
399 tp = p.style.Options.TimeInProgressPrecision
400 }
401 outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp)))
402
403 p.renderTrackerStatsETA(outStats, t, hint)
404 }
405
406 func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) {
407 if hint.isOverallTracker && !p.style.Visibility.ETAOverall {
408 return
409 }
410 if !hint.isOverallTracker && !p.style.Visibility.ETA {
411 return
412 }
413
414 tpETA := p.style.Options.ETAPrecision
415 if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA {
416 out.WriteString("; ")
417 out.WriteString(p.style.Options.ETAString)
418 out.WriteString(": ")
419 out.WriteString(p.style.Colors.Time.Sprint(eta))
420 }
421 }
422
View as plain text