diff --git a/cmd/demo-progress/demo.go b/cmd/demo-progress/demo.go index 795d7a6..290d00e 100644 --- a/cmd/demo-progress/demo.go +++ b/cmd/demo-progress/demo.go @@ -12,8 +12,9 @@ import ( var ( autoStop = flag.Bool("auto-stop", false, "Auto-stop rendering?") - randomFail = flag.Bool("rnd-fail", false, "Enable random failures") numTrackers = flag.Int("num-trackers", 13, "Number of Trackers") + randomFail = flag.Bool("rnd-fail", false, "Enable random failures in tracking") + randomLogs = flag.Bool("rnd-logs", false, "Enable random logs in the middle of tracking") messageColors = []text.Color{ text.FgRed, @@ -133,7 +134,16 @@ func main() { // wait for one or more trackers to become active (just blind-wait for a // second) and then keep watching until Rendering is in progress time.Sleep(time.Second) + messagesLogged := make(map[string]bool) for pw.IsRenderInProgress() { + if *randomLogs && pw.LengthDone()%3 == 0 { + logMsg := text.Faint.Sprintf("[INFO] done with %d trackers", pw.LengthDone()) + if !messagesLogged[logMsg] { + pw.Log(logMsg) + messagesLogged[logMsg] = true + } + } + // for manual-stop mode, stop when there are no more active trackers if !*autoStop && pw.LengthActive() == 0 { pw.Stop() diff --git a/progress/progress.go b/progress/progress.go index 12780ec..1b44192 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -1,6 +1,7 @@ package progress import ( + "fmt" "io" "os" "sync" @@ -28,6 +29,8 @@ type Progress struct { hideTracker bool hideValue bool hidePercentage bool + logsToRender []string + logsToRenderMutex sync.RWMutex messageWidth int numTrackersExpected int64 overallTracker *Tracker @@ -144,6 +147,17 @@ func (p *Progress) LengthInQueue() int { return out } +// Log appends a log to display above the active progress bars during the next +// refresh. +func (p *Progress) Log(msg string, a ...interface{}) { + if len(a) > 0 { + msg = fmt.Sprintf(msg, a...) + } + p.logsToRenderMutex.Lock() + p.logsToRender = append(p.logsToRender, msg) + p.logsToRenderMutex.Unlock() +} + // SetAutoStop toggles the auto-stop functionality. Auto-stop set to true would // mean that the Render() function will automatically stop once all currently // active Trackers reach their final states. When set to false, the client code diff --git a/progress/progress_test.go b/progress/progress_test.go index b0e3a24..3990a0c 100644 --- a/progress/progress_test.go +++ b/progress/progress_test.go @@ -87,6 +87,14 @@ func TestProgress_LengthInQueue(t *testing.T) { assert.Equal(t, 1, p.LengthInQueue()) } +func TestProgress_Log(t *testing.T) { + p := Progress{} + assert.Len(t, p.logsToRender, 0) + + p.Log("testing log") + assert.Len(t, p.logsToRender, 1) +} + func TestProgress_SetAutoStop(t *testing.T) { p := Progress{} assert.False(t, p.autoStop) diff --git a/progress/render.go b/progress/render.go index a778622..78ad5dc 100644 --- a/progress/render.go +++ b/progress/render.go @@ -272,39 +272,54 @@ func (p *Progress) renderTrackers(lastRenderLength int) int { p.moveCursorToTheTop(&out) } + // render the trackers that are done, and then the ones that are active + p.renderTrackersDoneAndActive(&out) + + // render the overall tracker + if p.showOverallTracker { + p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true}) + } + + // write the text to the output writer + _, _ = p.outputWriter.Write([]byte(out.String())) + + // stop if auto stop is enabled and there are no more active trackers + if p.autoStop && p.LengthActive() == 0 { + p.done <- true + } + + return out.Len() +} + +func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder) { // find the currently "active" and "done" trackers trackersActive, trackersDone := p.extractDoneAndActiveTrackers() // sort and render the done trackers for _, tracker := range trackersDone { - p.renderTracker(&out, tracker, renderHint{}) + p.renderTracker(out, tracker, renderHint{}) } p.trackersDoneMutex.Lock() p.trackersDone = append(p.trackersDone, trackersDone...) p.trackersDoneMutex.Unlock() + // render all the logs received and flush them out + p.logsToRenderMutex.Lock() + for _, log := range p.logsToRender { + out.WriteString(text.EraseLine.Sprint()) + out.WriteString(log) + out.WriteRune('\n') + } + p.logsToRender = nil + p.logsToRenderMutex.Unlock() + // sort and render the active trackers for _, tracker := range trackersActive { - p.renderTracker(&out, tracker, renderHint{}) + p.renderTracker(out, tracker, renderHint{}) } p.trackersActiveMutex.Lock() p.trackersActive = trackersActive p.trackersActiveMutex.Unlock() - - // render the overall tracker - if p.showOverallTracker { - p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true}) - } - - // write the text to the output writer - _, _ = p.outputWriter.Write([]byte(out.String())) - - // stop if auto stop is enabled and there are no more active trackers - if p.autoStop && p.LengthActive() == 0 { - p.done <- true - } - - return out.Len() } func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) { diff --git a/progress/render_test.go b/progress/render_test.go index 99fb6e1..1ab1b2a 100644 --- a/progress/render_test.go +++ b/progress/render_test.go @@ -611,6 +611,9 @@ func TestProgress_RenderSomeTrackers_WithOverallTracker(t *testing.T) { pw.ShowOverallTracker(true) pw.Style().Options.TimeOverallPrecision = time.Millisecond go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) + go func() { + pw.Log("some information about something that happened at %s", time.Now().Format(time.RFC3339)) + }() go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) @@ -623,6 +626,7 @@ func TestProgress_RenderSomeTrackers_WithOverallTracker(t *testing.T) { regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[K\[[.#]+] \[[\d.ms]+; ~ETA: [\d.ms]+`), + regexp.MustCompile(`some information about something that happened at \d\d\d\d`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { diff --git a/progress/writer.go b/progress/writer.go index 2a3b7ef..6f85148 100644 --- a/progress/writer.go +++ b/progress/writer.go @@ -15,6 +15,7 @@ type Writer interface { LengthActive() int LengthDone() int LengthInQueue() int + Log(msg string, a ...interface{}) SetAutoStop(autoStop bool) SetMessageWidth(width int) SetNumTrackersExpected(numTrackers int)