diff --git a/go.mod b/go.mod index 78412c5..f328cae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a34018d..0f23657 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/progressbar.go b/progressbar.go index 6402665..d8821a1 100644 --- a/progressbar.go +++ b/progressbar.go @@ -50,6 +50,7 @@ type state struct { counterTime time.Time counterNumSinceLast int64 counterLastTenRates []float64 + spinnerIdx int // the index of spinner maxLineWidth int currentBytes float64 @@ -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 @@ -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) { @@ -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, }, } @@ -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 } @@ -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 ", diff --git a/progressbar_test.go b/progressbar_test.go index ab143b5..c77aacc 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -5,7 +5,9 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "github.com/chengxilo/virtualterm" "io" + "log" "net/http" "os" "strings" @@ -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) } @@ -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) } @@ -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] } @@ -319,9 +326,11 @@ func ExampleOptionShowBytes_spinner() { /* Spinner test with iterations and count */ + buf := strings.Builder{} bar := NewOptions(-1, OptionSetWidth(10), OptionShowBytes(true), + OptionSetWriter(&buf), ) bar.Reset() @@ -329,7 +338,8 @@ func ExampleOptionShowBytes_spinner() { // 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] } @@ -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) @@ -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]) + } +}