From 243aacb476833bf7a4da7db4da4aa3a68ec71345 Mon Sep 17 00:00:00 2001 From: luo-cheng-xi Date: Mon, 2 Sep 2024 00:22:35 -0400 Subject: [PATCH] fix: change the logic of basic time state including startTime,lastShown and counterTime. --- progressbar.go | 46 +++++++++++++++++++++++++++++++++++++++++---- progressbar_test.go | 16 ++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/progressbar.go b/progressbar.go index 56c147e..25b454f 100644 --- a/progressbar.go +++ b/progressbar.go @@ -45,7 +45,7 @@ type state struct { isAltSaucerHead bool lastShown time.Time - startTime time.Time + startTime time.Time // time when the progress bar start working counterTime time.Time counterNumSinceLast int64 @@ -318,7 +318,11 @@ func NewOptions(max int, options ...Option) *ProgressBar { // NewOptions64 constructs a new instance of ProgressBar, with any options you specify func NewOptions64(max int64, options ...Option) *ProgressBar { b := ProgressBar{ - state: getBasicState(), + state: state{ + startTime: time.Time{}, + lastShown: time.Time{}, + counterTime: time.Time{}, + }, config: config{ writer: os.Stdout, theme: defaultTheme, @@ -486,6 +490,24 @@ func (p *ProgressBar) RenderBlank() error { return p.render() } +// StartWithoutRender will start the progress bar without rendering it +// this method is created for the use case where you want to start the progress +// but don't want to render it immediately. +// If you want to start the progress and render it immediately, use RenderBlank instead, +// or maybe you can use Add to start it automatically, but it will make the time calculation less precise. +func (p *ProgressBar) StartWithoutRender() { + p.lock.Lock() + defer p.lock.Unlock() + + if p.IsStarted() { + return + } + + p.state.startTime = time.Now() + // the counterTime should be set to the current time + p.state.counterTime = time.Now() +} + // Reset will reset the clock that is used // to calculate current time and the time left. func (p *ProgressBar) Reset() { @@ -567,6 +589,10 @@ func (p *ProgressBar) Add64(num int64) error { p.state.currentBytes += float64(num) + if p.state.counterTime.IsZero() { + p.state.counterTime = time.Now() + } + // reset the countdown timer every second to take rolling average p.state.counterNumSinceLast += num if time.Since(p.state.counterTime).Seconds() > 0.5 { @@ -669,13 +695,20 @@ func (p *ProgressBar) IsFinished() bool { return p.state.finished } +// IsStarted returns true if progress bar is started +func (p *ProgressBar) IsStarted() bool { + return !p.state.startTime.IsZero() +} + // render renders the progress bar, updating the maximum // rendered line width. this function is not thread-safe, // so it must be called with an acquired lock. func (p *ProgressBar) render() error { // make sure that the rendering is not happening too quickly // but always show if the currentNum reaches the max - if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() && + if !p.IsStarted() { + p.state.startTime = time.Now() + } else if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() && p.state.currentNum < p.config.max { return nil } @@ -738,7 +771,12 @@ func (p *ProgressBar) State() State { } s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max) s.CurrentBytes = p.state.currentBytes - s.SecondsSince = time.Since(p.state.startTime).Seconds() + if p.IsStarted() { + s.SecondsSince = time.Since(p.state.startTime).Seconds() + } else { + s.SecondsSince = 0 + } + if p.state.currentNum > 0 { s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum)) } diff --git a/progressbar_test.go b/progressbar_test.go index 7973ee6..ab143b5 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -375,6 +375,7 @@ func TestBarSmallBytes(t *testing.T) { func TestBarFastBytes(t *testing.T) { buf := strings.Builder{} bar := NewOptions64(1e8, OptionShowBytes(true), OptionShowCount(), OptionSetWidth(10), OptionSetWriter(&buf)) + bar.StartWithoutRender() time.Sleep(time.Millisecond) bar.Add(1e7) if !strings.Contains(buf.String(), " GB/s)") { @@ -889,6 +890,7 @@ func TestOptionFullWidth(t *testing.T) { t.Parallel() buf := strings.Builder{} bar := NewOptions(100, append(test.opts, []Option{OptionFullWidth(), OptionSetWriter(&buf)}...)...) + bar.StartWithoutRender() time.Sleep(1 * time.Second) bar.Add(10) time.Sleep(1 * time.Second) @@ -913,3 +915,17 @@ func TestHumanizeBytesIEC(t *testing.T) { amount, suffix = humanizeBytes(float64(56.78)*1024*1024*1024, true) assert.Equal(t, "57 GiB", fmt.Sprintf("%s%s", amount, suffix)) } + +func TestProgressBar_StartWithoutRender(t *testing.T) { + buf := strings.Builder{} + bar := NewOptions(100, OptionSetWriter(&buf)) + time.Sleep(1 * time.Second) + bar.StartWithoutRender() + time.Sleep(1 * time.Second) + bar.Add(10) + result := strings.TrimSpace(buf.String()) + expect := "10% |████ | [1s:9s]" + if result != expect { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } +}