...

Source file src/github.com/jedib0t/go-pretty/v6/progress/render.go

Documentation: github.com/jedib0t/go-pretty/v6/progress

     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() {
    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  				// always render the current state before finishing render in
    27  				// case it hasn't been shown yet
    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  	// move trackers waiting in queue to the active list
    67  	p.consumeQueuedTrackers()
    68  
    69  	// separate the active and done trackers
    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  	// calculate the overall tracker's progress value
    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  // generateTrackerStrDeterminate generates the tracker string for the case where
   103  // the Total value is known, and the progress percentage can be calculated.
   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  // generateTrackerStrDeterminate generates the tracker string for the case where
   138  // the Total value is unknown, and the progress percentage cannot be calculated.
   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  	// buffer all output into a strings.Builder object
   263  	var out strings.Builder
   264  	out.Grow(lastRenderLength)
   265  
   266  	// move up N times based on the number of active trackers
   267  	if lastRenderLength > 0 {
   268  		p.moveCursorToTheTop(&out)
   269  	}
   270  
   271  	// render the trackers that are done, and then the ones that are active
   272  	p.renderTrackersDoneAndActive(&out)
   273  
   274  	// render the overall tracker
   275  	if p.style.Visibility.TrackerOverall {
   276  		p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true})
   277  	}
   278  
   279  	// write the text to the output writer
   280  	_, _ = p.outputWriter.Write([]byte(out.String()))
   281  
   282  	// stop if auto stop is enabled and there are no more active trackers
   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  	// find the currently "active" and "done" trackers
   292  	trackersActive, trackersDone := p.extractDoneAndActiveTrackers()
   293  
   294  	// sort and render the done trackers
   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  	// render all the logs received and flush them out
   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  	// sort and render the active trackers
   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