From d1e3b99fd851ca875bed23cb57aea7b1a1a77e86 Mon Sep 17 00:00:00 2001 From: Jan Pfeifer Date: Thu, 3 Oct 2024 18:21:18 +0200 Subject: [PATCH 1/2] Added BarStartFilled and BarEndFilled fields to Theme struct. Added 3 predefined themes for users to choose from, if they want: ThemeDefault (no changes), ThemeASCII ("[===>...]") and ThemeUnicode using graphic symbols. Small documentation additions and fixes. --- progressbar.go | 74 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/progressbar.go b/progressbar.go index 657fd0d..54b971a 100644 --- a/progressbar.go +++ b/progressbar.go @@ -145,7 +145,45 @@ type Theme struct { SaucerPadding string BarStart string BarEnd string -} + + // BarStartFilled is used after the Bar starts filling, if set. Otherwise, it defaults to BarStart. + BarStartFilled string + + // BarEndFilled is used once the Bar finishes, if set. Otherwise, it defaults to BarEnd. + BarEndFilled string +} + +var ( + // ThemeDefault is given by default (if not changed with OptionSetTheme), and it looks like "|████ |". + ThemeDefault = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "|", BarEnd: "|"} + + // ThemeASCII is a predefined Theme that uses ASCII symbols. It looks like "[===>...]". + // Configure it with OptionSetTheme(ThemeASCII). + ThemeASCII = Theme{ + Saucer: "=", + SaucerHead: ">", + SaucerPadding: ".", + BarStart: "[", + BarEnd: "]", + } + + // ThemeUnicode is a predefined Theme that uses Unicode characters, displaying a graphic bar. + // It looks like "" (rendering will depend on font being used). + // It requires special symbols usually found in "nerd fonts" [2], or in Fira Code [1], and other sources. + // Configure it with OptionSetTheme(ThemeUnicode). + // + // [1] https://github.com/tonsky/FiraCode + // [2] https://www.nerdfonts.com/ + ThemeUnicode = Theme{ + Saucer: "\uEE04", //  + SaucerHead: "\uEE04", //  + SaucerPadding: "\uEE01", //  + BarStart: "\uEE00", //  + BarStartFilled: "\uEE03", //  + BarEnd: "\uEE02", //  + BarEndFilled: "\uEE05", //  + } +) // Option is the type all options need to adhere to type Option func(p *ProgressBar) @@ -185,7 +223,8 @@ func OptionSpinnerCustom(spinner []string) Option { } } -// OptionSetTheme sets the elements the bar is constructed of +// OptionSetTheme sets the elements the bar is constructed with. +// There are two pre-defined themes you can use: ThemeASCII and ThemeUnicode. func OptionSetTheme(t Theme) Option { return func(p *ProgressBar) { p.config.theme = t @@ -263,7 +302,7 @@ func OptionShowIts() Option { } } -// OptionShowElapsedOnFinish will keep the display of elapsed time on finish +// OptionShowElapsedTimeOnFinish will keep the display of elapsed time on finish. func OptionShowElapsedTimeOnFinish() Option { return func(p *ProgressBar) { p.config.showElapsedTimeOnFinish = true @@ -285,7 +324,7 @@ func OptionThrottle(duration time.Duration) Option { } } -// OptionClearOnFinish will clear the bar once its finished +// OptionClearOnFinish will clear the bar once its finished. func OptionClearOnFinish() Option { return func(p *ProgressBar) { p.config.clearOnFinish = true @@ -339,8 +378,6 @@ func OptionSetMaxDetailRow(row int) Option { } } -var defaultTheme = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "|", BarEnd: "|"} - // NewOptions constructs a new instance of ProgressBar, with any options you specify func NewOptions(max int, options ...Option) *ProgressBar { return NewOptions64(int64(max), options...) @@ -356,7 +393,7 @@ func NewOptions64(max int64, options ...Option) *ProgressBar { }, config: config{ writer: os.Stdout, - theme: defaultTheme, + theme: ThemeDefault, iterationString: "it", width: 40, max: max, @@ -860,6 +897,10 @@ func (p *ProgressBar) render() error { if err != nil { return err } + } else if !p.config.clearOnFinish { + // Since bar was cleared, re-render it. + io.Copy(p.config.writer, &p.config.stdBuffer) + renderProgressBar(p.config, &p.state) } return nil } @@ -1021,6 +1062,10 @@ func renderProgressBar(c config, s *state) (int, error) { } leftBrac, rightBrac, saucer, saucerHead := "", "", "", "" + barStart, barEnd := c.theme.BarStart, c.theme.BarEnd + if s.finished && c.theme.BarEndFilled != "" { + barEnd = c.theme.BarEndFilled + } // show time prediction in "current/total" seconds format switch { @@ -1057,6 +1102,9 @@ func renderProgressBar(c config, s *state) (int, error) { c.width = width - getStringWidth(c, c.description, true) - 10 - amend - sb.Len() - len(leftBrac) - len(rightBrac) s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width)) } + if (s.currentSaucerSize > 0 || s.currentPercent > 0) && c.theme.BarStartFilled != "" { + barStart = c.theme.BarStartFilled + } if s.currentSaucerSize > 0 { if c.ignoreLength { saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1) @@ -1138,11 +1186,11 @@ func renderProgressBar(c config, s *state) (int, error) { } else if rightBrac == "" { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", s.currentPercent, - c.theme.BarStart, + barStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, + barEnd, sb.String()) if (s.currentPercent == 100 && c.showElapsedTimeOnFinish) || c.elapsedTime { str = fmt.Sprintf("%s [%s]", str, leftBrac) @@ -1157,11 +1205,11 @@ func renderProgressBar(c config, s *state) (int, error) { if s.currentPercent == 100 { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", s.currentPercent, - c.theme.BarStart, + barStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, + barEnd, sb.String()) if c.showElapsedTimeOnFinish { @@ -1176,11 +1224,11 @@ func renderProgressBar(c config, s *state) (int, error) { } else { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]", s.currentPercent, - c.theme.BarStart, + barStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, + barEnd, sb.String(), leftBrac, rightBrac) From e0269136e63a9dd54c7675b1d831decc66af81fd Mon Sep 17 00:00:00 2001 From: Jan Pfeifer Date: Thu, 3 Oct 2024 18:55:15 +0200 Subject: [PATCH 2/2] Expanded Theme tests, and covered new BarStartFilled and BarEndFilled. --- progressbar.go | 14 +++++------ progressbar_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/progressbar.go b/progressbar.go index 54b971a..51be101 100644 --- a/progressbar.go +++ b/progressbar.go @@ -867,10 +867,12 @@ func (p *ProgressBar) render() error { } if !p.config.useANSICodes { - // first, clear the existing progress bar - err := clearProgressBar(p.config, p.state) - if err != nil { - return err + // first, clear the existing progress bar, if not yet finished. + if !p.state.finished { + err := clearProgressBar(p.config, p.state) + if err != nil { + return err + } } } @@ -897,10 +899,6 @@ func (p *ProgressBar) render() error { if err != nil { return err } - } else if !p.config.clearOnFinish { - // Since bar was cleared, re-render it. - io.Copy(p.config.writer, &p.config.stdBuffer) - renderProgressBar(p.config, &p.state) } return nil } diff --git a/progressbar_test.go b/progressbar_test.go index daeab36..11d886d 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -452,16 +452,69 @@ func TestOptionSetTheme(t *testing.T) { buf := strings.Builder{} bar := NewOptions( 10, - OptionSetTheme(Theme{Saucer: "#", SaucerPadding: "-", BarStart: ">", BarEnd: "<"}), + OptionSetTheme( + Theme{Saucer: "#", SaucerPadding: "-", + BarStart: ">", BarEnd: "<"}), OptionSetWidth(10), OptionSetWriter(&buf), ) + bar.RenderBlank() + result := strings.TrimSpace(buf.String()) + expect := "0% >----------<" + if strings.Index(result, expect) == -1 { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } + buf.Reset() + bar.Add(5) + result = strings.TrimSpace(buf.String()) + expect = "50% >#####-----< [0s:0s]" + if result != expect { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } + buf.Reset() + + bar.Finish() + result = strings.TrimSpace(buf.String()) + expect = "100% >##########<" + if strings.Index(result, expect) == -1 { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } +} + +func TestOptionSetThemeFilled(t *testing.T) { + buf := strings.Builder{} + bar := NewOptions( + 10, + OptionSetTheme( + Theme{Saucer: "#", SaucerPadding: "-", + BarStart: ">", BarStartFilled: "]", + BarEnd: "<", BarEndFilled: "["}), + OptionSetWidth(10), + OptionSetWriter(&buf), + ) + bar.RenderBlank() result := strings.TrimSpace(buf.String()) - expect := "50% >#####-----< [0s:0s]" + expect := "0% >----------<" + if strings.Index(result, expect) == -1 { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } + buf.Reset() + + bar.Add(5) + result = strings.TrimSpace(buf.String()) + expect = "50% ]#####-----< [0s:0s]" if result != expect { t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) } + buf.Reset() + + bar.Finish() + result = strings.TrimSpace(buf.String()) + expect = "100% ]##########[" + if strings.Index(result, expect) == -1 { + t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar) + } } // TestOptionSetPredictTime ensures that when predict time is turned off, the progress