Skip to content

Commit f1b3580

Browse files
authored
Merge pull request #199 from chengxilo/update-spinner
feat: make progressbar could update according to an interval or updat…
2 parents d773ff3 + 3d93361 commit f1b3580

File tree

4 files changed

+174
-31
lines changed

4 files changed

+174
-31
lines changed

go.mod

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
module github.com/schollz/progressbar/v3
22

33
require (
4-
github.com/davecgh/go-spew v1.1.1 // indirect
54
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
6-
github.com/mattn/go-isatty v0.0.20 // indirect
75
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
86
github.com/rivo/uniseg v0.4.7
9-
github.com/stretchr/testify v1.3.0
7+
github.com/stretchr/testify v1.9.0
108
golang.org/x/term v0.24.0
119
)
1210

13-
go 1.13
11+
require (
12+
github.com/chengxilo/virtualterm v1.0.4 // indirect
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/mattn/go-isatty v0.0.20 // indirect
15+
github.com/mattn/go-runewidth v0.0.16 // indirect
16+
github.com/pmezard/go-difflib v1.0.0 // indirect
17+
golang.org/x/sys v0.25.0 // indirect
18+
gopkg.in/yaml.v3 v3.0.1 // indirect
19+
)
20+
21+
go 1.22

go.sum

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1+
github.com/chengxilo/virtualterm v1.0.3 h1:Vycm/mKGeHuLXA4zK3XsaseOW7VMY6jJ88/9+XHSNcA=
2+
github.com/chengxilo/virtualterm v1.0.3/go.mod h1:wjAbIDvnp6Vc8hQoM7tt6fcdk0NiSaQBSoSRwMIpphs=
3+
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
4+
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
25
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
36
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
47
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
58
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
69
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
710
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
11+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
12+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
813
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
914
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
1015
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1116
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
17+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1218
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
1319
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
14-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15-
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
16-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
20+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
21+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1722
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1823
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
1924
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2025
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
2126
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
27+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
28+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
29+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
30+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

progressbar.go

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type state struct {
5050
counterTime time.Time
5151
counterNumSinceLast int64
5252
counterLastTenRates []float64
53+
spinnerIdx int // the index of spinner
5354

5455
maxLineWidth int
5556
currentBytes float64
@@ -105,6 +106,11 @@ type config struct {
105106
// spinnerTypeOptionUsed remembers if the spinnerType was changed manually
106107
spinnerTypeOptionUsed bool
107108

109+
// spinnerChangeInterval the change interval of spinner
110+
// if set this attribute to 0, the spinner only change when renderProgressBar was called
111+
// for example, each time when Add() was called,which will call renderProgressBar function
112+
spinnerChangeInterval time.Duration
113+
108114
// spinner represents the spinner as a slice of string
109115
spinner []string
110116

@@ -151,6 +157,18 @@ func OptionSetWidth(s int) Option {
151157
}
152158
}
153159

160+
// OptionSetSpinnerChangeInterval sets the spinner change interval
161+
// the spinner will change according to this value.
162+
// By default, this value is 100 * time.Millisecond
163+
// If you don't want to let this progressbar update by specified time interval
164+
// you can set this value to zero, then the spinner will change each time rendered,
165+
// such as when Add() or Describe() was called
166+
func OptionSetSpinnerChangeInterval(interval time.Duration) Option {
167+
return func(p *ProgressBar) {
168+
p.config.spinnerChangeInterval = interval
169+
}
170+
}
171+
154172
// OptionSpinnerType sets the type of spinner used for indeterminate bars
155173
func OptionSpinnerType(spinnerType int) Option {
156174
return func(p *ProgressBar) {
@@ -337,16 +355,17 @@ func NewOptions64(max int64, options ...Option) *ProgressBar {
337355
counterTime: time.Time{},
338356
},
339357
config: config{
340-
writer: os.Stdout,
341-
theme: defaultTheme,
342-
iterationString: "it",
343-
width: 40,
344-
max: max,
345-
throttleDuration: 0 * time.Nanosecond,
346-
elapsedTime: max == -1,
347-
predictTime: true,
348-
spinnerType: 9,
349-
invisible: false,
358+
writer: os.Stdout,
359+
theme: defaultTheme,
360+
iterationString: "it",
361+
width: 40,
362+
max: max,
363+
throttleDuration: 0 * time.Nanosecond,
364+
elapsedTime: max == -1,
365+
predictTime: true,
366+
spinnerType: 9,
367+
invisible: false,
368+
spinnerChangeInterval: 100 * time.Millisecond,
350369
},
351370
}
352371

@@ -374,6 +393,33 @@ func NewOptions64(max int64, options ...Option) *ProgressBar {
374393
b.RenderBlank()
375394
}
376395

396+
// if the render time interval attribute is set
397+
if b.config.spinnerChangeInterval != 0 {
398+
go func() {
399+
if b.config.invisible {
400+
return
401+
}
402+
if !b.config.ignoreLength {
403+
return
404+
}
405+
ticker := time.NewTicker(b.config.spinnerChangeInterval)
406+
defer ticker.Stop()
407+
for {
408+
select {
409+
case <-ticker.C:
410+
if b.IsFinished() {
411+
return
412+
}
413+
if b.IsStarted() {
414+
b.lock.Lock()
415+
b.render()
416+
b.lock.Unlock()
417+
}
418+
}
419+
}
420+
}()
421+
}
422+
377423
return &b
378424
}
379425

@@ -1058,7 +1104,16 @@ func renderProgressBar(c config, s *state) (int, error) {
10581104
if len(c.spinner) > 0 {
10591105
selectedSpinner = c.spinner
10601106
}
1061-
spinner := selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(selectedSpinner)))))]
1107+
1108+
var spinner string
1109+
if c.spinnerChangeInterval != 0 {
1110+
// if the spinner is changed according to an interval, calculate it
1111+
spinner = selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Nanoseconds()/c.spinnerChangeInterval.Nanoseconds()), float64(len(selectedSpinner)))))]
1112+
} else {
1113+
// if the spinner is changed according to the number render was called
1114+
spinner = selectedSpinner[s.spinnerIdx]
1115+
s.spinnerIdx = (s.spinnerIdx + 1) % len(selectedSpinner)
1116+
}
10621117
if c.elapsedTime {
10631118
if c.showDescriptionAtLineEnd {
10641119
str = fmt.Sprintf("\r%s %s [%s] %s ",

progressbar_test.go

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"crypto/md5"
66
"encoding/hex"
77
"fmt"
8+
"github.com/chengxilo/virtualterm"
89
"io"
10+
"log"
911
"net/http"
1012
"os"
1113
"strings"
@@ -101,10 +103,8 @@ func TestSpinnerClearOnFinish(t *testing.T) {
101103
bar.Add(10)
102104
time.Sleep(1 * time.Second)
103105
bar.Finish()
104-
result := buf.String()
105-
expect := "" +
106-
"\r- (10 B, 10 B/s, 10 it/s) [1s] " +
107-
"\r \r"
106+
result, _ := virtualterm.Process(buf.String())
107+
expect := " "
108108
if result != expect {
109109
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
110110
}
@@ -127,11 +127,12 @@ func TestSpinnerFinish(t *testing.T) {
127127
bar.Add(10)
128128
time.Sleep(1 * time.Second)
129129
bar.Finish()
130-
result := buf.String()
131-
expect := "" +
132-
"\r- (10 B, 10 B/s, 10 it/s) [1s] " +
133-
"\r \r" +
134-
"\r| (10 B, 5 B/s, 5 it/s) [2s] "
130+
result, err := virtualterm.Process(buf.String())
131+
if err != nil {
132+
t.Error(err)
133+
}
134+
// the "\r \r"
135+
expect := "| (10 B, 5 B/s, 5 it/s) [2s] "
135136
if result != expect {
136137
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
137138
}
@@ -214,15 +215,21 @@ func ExampleOptionShowIts_spinner() {
214215
/*
215216
Spinner test with iteration count and iteration rate
216217
*/
218+
vt := virtualterm.NewDefault()
217219
bar := NewOptions(-1,
218220
OptionSetWidth(10),
219221
OptionShowIts(),
220222
OptionShowCount(),
223+
OptionSetWriter(&vt),
221224
)
222225
bar.Reset()
223226
time.Sleep(1 * time.Second)
224227
bar.Add(5)
225-
228+
s, err := vt.String()
229+
if err != nil {
230+
log.Fatal(err)
231+
}
232+
fmt.Print(s)
226233
// Output:
227234
// - (5/-, 5 it/s) [1s]
228235
}
@@ -319,17 +326,20 @@ func ExampleOptionShowBytes_spinner() {
319326
/*
320327
Spinner test with iterations and count
321328
*/
329+
buf := strings.Builder{}
322330
bar := NewOptions(-1,
323331
OptionSetWidth(10),
324332
OptionShowBytes(true),
333+
OptionSetWriter(&buf),
325334
)
326335

327336
bar.Reset()
328337
time.Sleep(1 * time.Second)
329338
// since 10 is the width and we don't know the max bytes
330339
// it will do a infinite scrolling.
331340
bar.Add(11)
332-
341+
result, _ := virtualterm.Process(buf.String())
342+
fmt.Print(result)
333343
// Output:
334344
// - (11 B/s) [1s]
335345
}
@@ -495,7 +505,11 @@ func TestOptionSetElapsedTime_spinner(t *testing.T) {
495505
bar.Reset()
496506
time.Sleep(1 * time.Second)
497507
bar.Add(5)
498-
result := strings.TrimSpace(buf.String())
508+
result, err := virtualterm.Process(buf.String())
509+
result = strings.TrimSpace(result)
510+
if err != nil {
511+
t.Fatal(err)
512+
}
499513
expect := "- (5/-, 5 it/s)"
500514
if result != expect {
501515
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
@@ -929,3 +943,60 @@ func TestProgressBar_StartWithoutRender(t *testing.T) {
929943
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
930944
}
931945
}
946+
947+
func TestOptionSetSpinnerChangeInterval(t *testing.T) {
948+
interval := 1000 * time.Millisecond
949+
vt := virtualterm.NewDefault()
950+
actuals := make([]string, 0, 8)
951+
expecteds := []string{
952+
"◐ test [0s]",
953+
"◓ test [1s]",
954+
"◑ test [2s]",
955+
"◒ test [3s]",
956+
"◐ test [4s]",
957+
"◓ test [5s]",
958+
"◑ test [6s]",
959+
"◒ test [7s]",
960+
}
961+
bar := NewOptions(-1,
962+
OptionSetDescription("test"),
963+
OptionSpinnerType(7),
964+
OptionSetWriter(&vt),
965+
OptionSetSpinnerChangeInterval(interval))
966+
bar.Add(1)
967+
for i := 0; i < 8; i++ {
968+
s, _ := vt.String()
969+
s = strings.TrimSpace(s)
970+
actuals = append(actuals, s)
971+
// sleep 50 ms more to make sure to go to next interval each time
972+
time.Sleep(1050 * time.Millisecond)
973+
}
974+
for i := range actuals {
975+
assert.Equal(t, expecteds[i], actuals[i])
976+
}
977+
}
978+
979+
func TestOptionSetSpinnerChangeIntervalZero(t *testing.T) {
980+
vt := virtualterm.NewDefault()
981+
bar := NewOptions(-1,
982+
OptionSetDescription("test"),
983+
OptionSpinnerType(7),
984+
OptionSetWriter(&vt),
985+
OptionSetSpinnerChangeInterval(0))
986+
actuals := make([]string, 0, 5)
987+
expected := []string{
988+
"◐ test [0s]",
989+
"◓ test [1s]",
990+
"◑ test [2s]",
991+
"◒ test [3s]",
992+
"◐ test [4s]",
993+
}
994+
for i := 0; i < 5; i++ {
995+
bar.Add(1)
996+
s, _ := vt.String()
997+
s = strings.TrimSpace(s)
998+
}
999+
for i := range actuals {
1000+
assert.Equal(t, expected[i], actuals[i])
1001+
}
1002+
}

0 commit comments

Comments
 (0)