Skip to content

Commit

Permalink
Merge pull request #199 from chengxilo/update-spinner
Browse files Browse the repository at this point in the history
feat: make progressbar could update according to an interval or updat…
  • Loading branch information
schollz authored Sep 23, 2024
2 parents d773ff3 + 3d93361 commit f1b3580
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 31 deletions.
16 changes: 12 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
module github.com/schollz/progressbar/v3

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
github.com/rivo/uniseg v0.4.7
github.com/stretchr/testify v1.3.0
github.com/stretchr/testify v1.9.0
golang.org/x/term v0.24.0
)

go 1.13
require (
github.com/chengxilo/virtualterm v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

go 1.22
17 changes: 13 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/chengxilo/virtualterm v1.0.3 h1:Vycm/mKGeHuLXA4zK3XsaseOW7VMY6jJ88/9+XHSNcA=
github.com/chengxilo/virtualterm v1.0.3/go.mod h1:wjAbIDvnp6Vc8hQoM7tt6fcdk0NiSaQBSoSRwMIpphs=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
77 changes: 66 additions & 11 deletions progressbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type state struct {
counterTime time.Time
counterNumSinceLast int64
counterLastTenRates []float64
spinnerIdx int // the index of spinner

maxLineWidth int
currentBytes float64
Expand Down Expand Up @@ -105,6 +106,11 @@ type config struct {
// spinnerTypeOptionUsed remembers if the spinnerType was changed manually
spinnerTypeOptionUsed bool

// spinnerChangeInterval the change interval of spinner
// if set this attribute to 0, the spinner only change when renderProgressBar was called
// for example, each time when Add() was called,which will call renderProgressBar function
spinnerChangeInterval time.Duration

// spinner represents the spinner as a slice of string
spinner []string

Expand Down Expand Up @@ -151,6 +157,18 @@ func OptionSetWidth(s int) Option {
}
}

// OptionSetSpinnerChangeInterval sets the spinner change interval
// the spinner will change according to this value.
// By default, this value is 100 * time.Millisecond
// If you don't want to let this progressbar update by specified time interval
// you can set this value to zero, then the spinner will change each time rendered,
// such as when Add() or Describe() was called
func OptionSetSpinnerChangeInterval(interval time.Duration) Option {
return func(p *ProgressBar) {
p.config.spinnerChangeInterval = interval
}
}

// OptionSpinnerType sets the type of spinner used for indeterminate bars
func OptionSpinnerType(spinnerType int) Option {
return func(p *ProgressBar) {
Expand Down Expand Up @@ -337,16 +355,17 @@ func NewOptions64(max int64, options ...Option) *ProgressBar {
counterTime: time.Time{},
},
config: config{
writer: os.Stdout,
theme: defaultTheme,
iterationString: "it",
width: 40,
max: max,
throttleDuration: 0 * time.Nanosecond,
elapsedTime: max == -1,
predictTime: true,
spinnerType: 9,
invisible: false,
writer: os.Stdout,
theme: defaultTheme,
iterationString: "it",
width: 40,
max: max,
throttleDuration: 0 * time.Nanosecond,
elapsedTime: max == -1,
predictTime: true,
spinnerType: 9,
invisible: false,
spinnerChangeInterval: 100 * time.Millisecond,
},
}

Expand Down Expand Up @@ -374,6 +393,33 @@ func NewOptions64(max int64, options ...Option) *ProgressBar {
b.RenderBlank()
}

// if the render time interval attribute is set
if b.config.spinnerChangeInterval != 0 {
go func() {
if b.config.invisible {
return
}
if !b.config.ignoreLength {
return
}
ticker := time.NewTicker(b.config.spinnerChangeInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if b.IsFinished() {
return
}
if b.IsStarted() {
b.lock.Lock()
b.render()
b.lock.Unlock()
}
}
}
}()
}

return &b
}

Expand Down Expand Up @@ -1058,7 +1104,16 @@ func renderProgressBar(c config, s *state) (int, error) {
if len(c.spinner) > 0 {
selectedSpinner = c.spinner
}
spinner := selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(selectedSpinner)))))]

var spinner string
if c.spinnerChangeInterval != 0 {
// if the spinner is changed according to an interval, calculate it
spinner = selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Nanoseconds()/c.spinnerChangeInterval.Nanoseconds()), float64(len(selectedSpinner)))))]
} else {
// if the spinner is changed according to the number render was called
spinner = selectedSpinner[s.spinnerIdx]
s.spinnerIdx = (s.spinnerIdx + 1) % len(selectedSpinner)
}
if c.elapsedTime {
if c.showDescriptionAtLineEnd {
str = fmt.Sprintf("\r%s %s [%s] %s ",
Expand Down
95 changes: 83 additions & 12 deletions progressbar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/chengxilo/virtualterm"
"io"
"log"
"net/http"
"os"
"strings"
Expand Down Expand Up @@ -101,10 +103,8 @@ func TestSpinnerClearOnFinish(t *testing.T) {
bar.Add(10)
time.Sleep(1 * time.Second)
bar.Finish()
result := buf.String()
expect := "" +
"\r- (10 B, 10 B/s, 10 it/s) [1s] " +
"\r \r"
result, _ := virtualterm.Process(buf.String())
expect := " "
if result != expect {
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
}
Expand All @@ -127,11 +127,12 @@ func TestSpinnerFinish(t *testing.T) {
bar.Add(10)
time.Sleep(1 * time.Second)
bar.Finish()
result := buf.String()
expect := "" +
"\r- (10 B, 10 B/s, 10 it/s) [1s] " +
"\r \r" +
"\r| (10 B, 5 B/s, 5 it/s) [2s] "
result, err := virtualterm.Process(buf.String())
if err != nil {
t.Error(err)
}
// the "\r \r"
expect := "| (10 B, 5 B/s, 5 it/s) [2s] "
if result != expect {
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
}
Expand Down Expand Up @@ -214,15 +215,21 @@ func ExampleOptionShowIts_spinner() {
/*
Spinner test with iteration count and iteration rate
*/
vt := virtualterm.NewDefault()
bar := NewOptions(-1,
OptionSetWidth(10),
OptionShowIts(),
OptionShowCount(),
OptionSetWriter(&vt),
)
bar.Reset()
time.Sleep(1 * time.Second)
bar.Add(5)

s, err := vt.String()
if err != nil {
log.Fatal(err)
}
fmt.Print(s)
// Output:
// - (5/-, 5 it/s) [1s]
}
Expand Down Expand Up @@ -319,17 +326,20 @@ func ExampleOptionShowBytes_spinner() {
/*
Spinner test with iterations and count
*/
buf := strings.Builder{}
bar := NewOptions(-1,
OptionSetWidth(10),
OptionShowBytes(true),
OptionSetWriter(&buf),
)

bar.Reset()
time.Sleep(1 * time.Second)
// since 10 is the width and we don't know the max bytes
// it will do a infinite scrolling.
bar.Add(11)

result, _ := virtualterm.Process(buf.String())
fmt.Print(result)
// Output:
// - (11 B/s) [1s]
}
Expand Down Expand Up @@ -495,7 +505,11 @@ func TestOptionSetElapsedTime_spinner(t *testing.T) {
bar.Reset()
time.Sleep(1 * time.Second)
bar.Add(5)
result := strings.TrimSpace(buf.String())
result, err := virtualterm.Process(buf.String())
result = strings.TrimSpace(result)
if err != nil {
t.Fatal(err)
}
expect := "- (5/-, 5 it/s)"
if result != expect {
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
Expand Down Expand Up @@ -929,3 +943,60 @@ func TestProgressBar_StartWithoutRender(t *testing.T) {
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
}
}

func TestOptionSetSpinnerChangeInterval(t *testing.T) {
interval := 1000 * time.Millisecond
vt := virtualterm.NewDefault()
actuals := make([]string, 0, 8)
expecteds := []string{
"◐ test [0s]",
"◓ test [1s]",
"◑ test [2s]",
"◒ test [3s]",
"◐ test [4s]",
"◓ test [5s]",
"◑ test [6s]",
"◒ test [7s]",
}
bar := NewOptions(-1,
OptionSetDescription("test"),
OptionSpinnerType(7),
OptionSetWriter(&vt),
OptionSetSpinnerChangeInterval(interval))
bar.Add(1)
for i := 0; i < 8; i++ {
s, _ := vt.String()
s = strings.TrimSpace(s)
actuals = append(actuals, s)
// sleep 50 ms more to make sure to go to next interval each time
time.Sleep(1050 * time.Millisecond)
}
for i := range actuals {
assert.Equal(t, expecteds[i], actuals[i])
}
}

func TestOptionSetSpinnerChangeIntervalZero(t *testing.T) {
vt := virtualterm.NewDefault()
bar := NewOptions(-1,
OptionSetDescription("test"),
OptionSpinnerType(7),
OptionSetWriter(&vt),
OptionSetSpinnerChangeInterval(0))
actuals := make([]string, 0, 5)
expected := []string{
"◐ test [0s]",
"◓ test [1s]",
"◑ test [2s]",
"◒ test [3s]",
"◐ test [4s]",
}
for i := 0; i < 5; i++ {
bar.Add(1)
s, _ := vt.String()
s = strings.TrimSpace(s)
}
for i := range actuals {
assert.Equal(t, expected[i], actuals[i])
}
}

0 comments on commit f1b3580

Please sign in to comment.