Skip to content

Commit

Permalink
progress: auto-trim output to fit terminal width
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Jan 7, 2024
1 parent f26db87 commit 55479c4
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 54 deletions.
4 changes: 2 additions & 2 deletions cmd/demo-progress/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ func main() {
// instantiate a Progress Writer and set up the options
pw := progress.NewWriter()
pw.SetAutoStop(*flagAutoStop)
pw.SetTrackerLength(25)
pw.SetMessageWidth(24)
pw.SetMessageLength(24)
pw.SetNumTrackersExpected(*flagNumTrackers)
pw.SetSortBy(progress.SortByPercentDsc)
pw.SetStyle(progress.StyleDefault)
pw.SetTrackerLength(25)
pw.SetTrackerPosition(progress.PositionRight)
pw.SetUpdateFrequency(time.Millisecond * 100)
pw.Style().Colors = progress.StyleColorsExample
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ require (
github.com/mattn/go-runewidth v0.0.13
github.com/pkg/profile v1.6.0
github.com/stretchr/testify v1.7.4
golang.org/x/sys v0.1.0
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
74 changes: 62 additions & 12 deletions progress/progress.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package progress

import (
"context"
"fmt"
"io"
"os"
"sync"
"time"
"unicode/utf8"

"github.com/jedib0t/go-pretty/v6/text"
"golang.org/x/term"

Check failure on line 12 in progress/progress.go

View workflow job for this annotation

GitHub Actions / build

//go:build comment without // +build comment

Check failure on line 12 in progress/progress.go

View workflow job for this annotation

GitHub Actions / build

//go:build comment without // +build comment
)

var (
Expand All @@ -23,24 +24,27 @@ var (
// Progress helps track progress for one or more tasks.
type Progress struct {
autoStop bool
done chan bool
lengthMessage int
lengthProgress int
lengthProgressOverall int
lengthTracker int
logsToRender []string
logsToRenderMutex sync.RWMutex
messageWidth int
numTrackersExpected int64
outputWriter io.Writer
overallTracker *Tracker
overallTrackerMutex sync.RWMutex
pinnedMessages []string
pinnedMessageMutex sync.RWMutex
pinnedMessageNumLines int
renderContext context.Context
renderContextCancel context.CancelFunc
renderInProgress bool
renderInProgressMutex sync.RWMutex
sortBy SortBy
style *Style
terminalWidth int
terminalWidthOverride int
trackerPosition Position
trackersActive []*Tracker
trackersActiveMutex sync.RWMutex
Expand Down Expand Up @@ -168,11 +172,19 @@ func (p *Progress) SetAutoStop(autoStop bool) {
p.autoStop = autoStop
}

// SetMessageWidth sets the (printed) length of the tracker message. Any message
// longer the specified width will be snipped abruptly. Any message shorter than
// SetMessageLength sets the (printed) length of the tracker message. Any
// message longer the specified length will be snipped. Any message shorter than
// the specified width will be padded with spaces.
func (p *Progress) SetMessageLength(length int) {
p.lengthMessage = length
}

// SetMessageWidth sets the (printed) length of the tracker message. Any message
// longer the specified width will be snipped. Any message shorter than the
// specified width will be padded with spaces.
// Deprecated: in favor of SetMessageLength(length)
func (p *Progress) SetMessageWidth(width int) {
p.messageWidth = width
p.lengthMessage = width
}

// SetNumTrackersExpected sets the expected number of trackers to be tracked.
Expand Down Expand Up @@ -209,6 +221,12 @@ func (p *Progress) SetStyle(style Style) {
p.style = &style
}

// SetTerminalWidth sets up a sticky terminal width and prevents the Progress
// Writer from polling for the real width during render.
func (p *Progress) SetTerminalWidth(width int) {
p.terminalWidthOverride = width
}

// SetTrackerLength sets the text-length of all the Trackers.
func (p *Progress) SetTrackerLength(length int) {
p.lengthTracker = length
Expand Down Expand Up @@ -266,7 +284,7 @@ func (p *Progress) ShowValue(show bool) {
// Stop stops the Render() logic that is in progress.
func (p *Progress) Stop() {
if p.IsRenderInProgress() {
p.done <- true
p.renderContextCancel()
}
}

Expand All @@ -279,6 +297,13 @@ func (p *Progress) Style() *Style {
return p.style
}

func (p *Progress) getTerminalWidth() int {
if p.terminalWidthOverride > 0 {
return p.terminalWidthOverride
}
return p.terminalWidth
}

func (p *Progress) initForRender() {
// pick a default style
p.Style()
Expand All @@ -287,7 +312,7 @@ func (p *Progress) initForRender() {
}

// reset the signals
p.done = make(chan bool, 1)
p.renderContext, p.renderContextCancel = context.WithCancel(context.Background())

// pick default lengths if no valid ones set
if p.lengthTracker <= 0 {
Expand All @@ -297,13 +322,15 @@ func (p *Progress) initForRender() {
// calculate length of the actual progress bar by discounting the left/right
// border/box chars
p.lengthProgress = p.lengthTracker -
utf8.RuneCountInString(p.style.Chars.BoxLeft) -
utf8.RuneCountInString(p.style.Chars.BoxRight)
p.lengthProgressOverall = p.messageWidth +
text.RuneWidthWithoutEscSequences(p.style.Chars.BoxLeft) -
text.RuneWidthWithoutEscSequences(p.style.Chars.BoxRight)
p.lengthProgressOverall = p.lengthMessage +
text.RuneWidthWithoutEscSequences(p.style.Options.Separator) +
p.lengthProgress + 1
if p.style.Visibility.Percentage {
p.lengthProgressOverall += text.RuneWidthWithoutEscSequences(fmt.Sprintf(p.style.Options.PercentFormat, 0.0))
p.lengthProgressOverall += text.RuneWidthWithoutEscSequences(
fmt.Sprintf(p.style.Options.PercentFormat, 0.0),
)
}

// if not output write has been set, output to STDOUT
Expand All @@ -315,6 +342,29 @@ func (p *Progress) initForRender() {
if p.updateFrequency <= 0 {
p.updateFrequency = DefaultUpdateFrequency
}

// get the current terminal size for preventing roll-overs, and do this in a
// background loop until end of render
go p.watchTerminalSize() // needs p.updateFrequency
}

func (p *Progress) updateTerminalSize() {
p.terminalWidth, _, _ = term.GetSize(int(os.Stdout.Fd()))
}

func (p *Progress) watchTerminalSize() {
// once
p.updateTerminalSize()
// until end of time
ticker := time.NewTicker(time.Second / 10)
for {
select {
case <-ticker.C:
p.updateTerminalSize()
case <-p.renderContext.Done():
return
}
}
}

// renderHint has hints for the Render*() logic
Expand Down
17 changes: 13 additions & 4 deletions progress/progress_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package progress

import (
"context"
"math"
"os"
"testing"
Expand Down Expand Up @@ -143,6 +144,16 @@ func TestProgress_SetStyle(t *testing.T) {
assert.Equal(t, StyleCircle.Name, p.Style().Name)
}

func TestProgress_SetMessageLength(t *testing.T) {
p := Progress{}
assert.Equal(t, 0, p.lengthMessage)

p.SetMessageLength(80)
assert.Equal(t, 80, p.lengthMessage)
p.SetMessageWidth(81)
assert.Equal(t, 81, p.lengthMessage)
}

func TestProgress_SetTrackerLength(t *testing.T) {
p := Progress{}
assert.Equal(t, 0, p.lengthTracker)
Expand Down Expand Up @@ -222,13 +233,11 @@ func TestProgress_ShowValue(t *testing.T) {
}

func TestProgress_Stop(t *testing.T) {
doneChannel := make(chan bool, 1)

p := Progress{}
p.done = doneChannel
p.renderContext, p.renderContextCancel = context.WithCancel(context.Background())
p.renderInProgress = true
p.Stop()
assert.True(t, <-doneChannel)
assert.NotNil(t, <-p.renderContext.Done())
}

func TestProgress_Style(t *testing.T) {
Expand Down
37 changes: 23 additions & 14 deletions progress/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (p *Progress) Render() {
select {
case <-ticker.C:
lastRenderLength = p.renderTrackers(lastRenderLength)
case <-p.done:
case <-p.renderContext.Done():
// always render the current state before finishing render in
// case it hasn't been shown yet
p.renderTrackers(lastRenderLength)
Expand Down Expand Up @@ -180,7 +180,11 @@ func (p *Progress) renderPinnedMessages(out *strings.Builder) {
numLines := len(p.pinnedMessages)
for _, msg := range p.pinnedMessages {
msg = strings.TrimSpace(msg)
out.WriteString(p.style.Colors.Pinned.Sprint(msg))
msg = p.style.Colors.Pinned.Sprint(msg)
if width := p.getTerminalWidth(); width > 0 {
msg = text.Trim(msg, width)
}
out.WriteString(msg)
out.WriteRune('\n')

numLines += strings.Count(msg, "\n")
Expand All @@ -192,28 +196,37 @@ func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHi
message := t.message()
message = strings.ReplaceAll(message, "\t", " ")
message = strings.ReplaceAll(message, "\r", "")
if p.messageWidth > 0 {
if p.lengthMessage > 0 {
messageLen := text.RuneWidthWithoutEscSequences(message)
if messageLen < p.messageWidth {
message = text.Pad(message, p.messageWidth, ' ')
if messageLen < p.lengthMessage {
message = text.Pad(message, p.lengthMessage, ' ')
} else {
message = text.Snip(message, p.messageWidth, p.style.Options.SnipIndicator)
message = text.Snip(message, p.lengthMessage, p.style.Options.SnipIndicator)
}
}

tOut := &strings.Builder{}
tOut.Grow(p.lengthProgressOverall)
if hint.isOverallTracker {
if !t.IsDone() {
hint := renderHint{hideValue: true, isOverallTracker: true}
p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgressOverall, hint), hint)
}
} else {
if t.IsDone() {
p.renderTrackerDone(out, t, message)
p.renderTrackerDone(tOut, t, message)
} else {
hint := renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value}
p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
p.renderTrackerProgress(tOut, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint)
}
}

outStr := tOut.String()
if width := p.getTerminalWidth(); width > 0 {
outStr = text.Trim(outStr, width)
}
out.WriteString(outStr)
out.WriteRune('\n')
}

func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message string) {
Expand All @@ -225,7 +238,6 @@ func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message s
out.WriteString(p.style.Colors.Error.Sprint(p.style.Options.ErrorString))
}
p.renderTrackerStats(out, t, renderHint{hideTime: !p.style.Visibility.Time, hideValue: !p.style.Visibility.Value})
out.WriteRune('\n')
}

func (p *Progress) renderTrackerMessage(out *strings.Builder, t *Tracker, message string) {
Expand All @@ -252,7 +264,6 @@ func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, messa
if hint.isOverallTracker {
out.WriteString(p.style.Colors.Tracker.Sprint(trackerStr))
p.renderTrackerStats(out, t, hint)
out.WriteRune('\n')
} else if p.trackerPosition == PositionRight {
p.renderTrackerMessage(out, t, message)
out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
Expand All @@ -261,7 +272,6 @@ func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, messa
out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr))
}
p.renderTrackerStats(out, t, hint)
out.WriteRune('\n')
} else {
p.renderTrackerPercentage(out, t)
if p.style.Visibility.Tracker {
Expand All @@ -270,7 +280,6 @@ func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, messa
p.renderTrackerStats(out, t, hint)
out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator))
p.renderTrackerMessage(out, t, message)
out.WriteRune('\n')
}
}

Expand Down Expand Up @@ -301,7 +310,7 @@ func (p *Progress) renderTrackers(lastRenderLength int) int {

// stop if auto stop is enabled and there are no more active trackers
if p.autoStop && p.LengthActive() == 0 {
p.done <- true
p.renderContextCancel()
}

return out.Len()
Expand Down
Loading

0 comments on commit 55479c4

Please sign in to comment.