diff --git a/bar.go b/bar.go index 5a506fc..b801e7f 100644 --- a/bar.go +++ b/bar.go @@ -52,6 +52,7 @@ total int64 current int64 runes barRunes + spinner []rune trimLeftSpace bool trimRightSpace bool toComplete bool @@ -345,7 +346,7 @@ return io.MultiReader(s.bufP, s.bufA) } - s.fillBar(s.width) + s.fill(s.width) barCount := utf8.RuneCount(s.bufB.Bytes()) totalCount := prependCount + barCount + appendCount if spaceCount := 0; totalCount > termWidth { @@ -355,10 +356,35 @@ if !s.trimRightSpace { spaceCount++ } - s.fillBar(termWidth - prependCount - appendCount - spaceCount) + s.fill(termWidth - prependCount - appendCount - spaceCount) } return io.MultiReader(s.bufP, s.bufB, s.bufA) +} + +func (s *bState) fill(width int) { + if len(s.spinner) != 0 { + s.fillSpinner(width) + } else { + s.fillBar(width) + } +} + +func (s *bState) fillSpinner(width int) { + s.bufB.Reset() + + if !s.trimLeftSpace { + s.bufB.WriteByte(' ') + } + + spin := []byte(string(s.spinner[s.current%int64(len(s.spinner))])) + for _, b := range spin { + s.bufB.WriteByte(b) + } + + for i := len(spin); i < width; i++ { + s.bufB.WriteRune(' ') + } } func (s *bState) fillBar(width int) { diff --git a/bar_option.go b/bar_option.go index e33bce4..800210d 100644 --- a/bar_option.go +++ b/bar_option.go @@ -111,6 +111,12 @@ } } +func barSpinner(spinner string) BarOption { + return func(s *bState) { + s.spinner = []rune(spinner) + } +} + func barWidth(w int) BarOption { return func(s *bState) { s.width = w diff --git a/examples/spinner/main.go b/examples/spinner/main.go new file mode 100644 index 0000000..e21163e --- /dev/null +++ b/examples/spinner/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb" + "github.com/vbauerster/mpb/decor" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func main() { + var wg sync.WaitGroup + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithSpinner("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"), + ) + total, numBars := 100, 3 + wg.Add(numBars) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 60 + decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + start := time.Now() + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + // ewma based decorators require work duration measurement + bar.IncrBy(1, time.Since(start)) + } + }() + } + // wait for all bars to complete and flush + p.Wait() +} diff --git a/options.go b/options.go index 05d2ecf..9240519 100644 --- a/options.go +++ b/options.go @@ -29,6 +29,13 @@ if w >= 0 { s.width = w } + } +} + +// WithSpinner overrides default bar format to use a spinner +func WithSpinner(spinner string) ProgressOption { + return func(s *pState) { + s.spinner = spinner } } diff --git a/progress.go b/progress.go index d95fe45..7387974 100644 --- a/progress.go +++ b/progress.go @@ -37,6 +37,7 @@ idCounter int width int format string + spinner string rr time.Duration cw *cwriter.Writer pMatrix map[int][]chan int @@ -89,6 +90,9 @@ select { case p.operateState <- func(s *pState) { options = append(options, barWidth(s.width), barFormat(s.format)) + if s.spinner != "" { + options = append(options, barSpinner(s.spinner)) + } b := newBar(p.wg, s.idCounter, total, s.cancel, options...) if b.runningBar != nil { s.waitBars[b.runningBar] = b