package progressbar import ( "errors" "fmt" "io" "math" "os" "regexp" "strings" "sync" "time" "github.com/mattn/go-runewidth" "github.com/mitchellh/colorstring" "golang.org/x/term" ) // ProgressBar is a thread-safe, simple // progress bar type ProgressBar struct { state state config config lock sync.Mutex } // State is the basic properties of the bar type State struct { CurrentPercent float64 CurrentBytes float64 SecondsSince float64 SecondsLeft float64 KBsPerSecond float64 } type state struct { currentNum int64 currentPercent int lastPercent int currentSaucerSize int isAltSaucerHead bool lastShown time.Time startTime time.Time counterTime time.Time counterNumSinceLast int64 counterLastTenRates []float64 maxLineWidth int currentBytes float64 finished bool exit bool // Progress bar exit halfway rendered string } type config struct { max int64 // max number of the counter maxHumanized string maxHumanizedSuffix string width int writer io.Writer theme Theme renderWithBlankState bool description string iterationString string ignoreLength bool // ignoreLength if max bytes not known // whether the output is expected to contain color codes colorCodes bool // show rate of change in kB/sec or MB/sec showBytes bool // show the iterations per second showIterationsPerSecond bool showIterationsCount bool // whether the progress bar should show elapsed time. // always enabled if predictTime is true. elapsedTime bool showElapsedTimeOnFinish bool // whether the progress bar should attempt to predict the finishing // time of the progress based on the start time and the average // number of seconds between increments. predictTime bool // minimum time to wait in between updates throttleDuration time.Duration // clear bar once finished clearOnFinish bool // spinnerType should be a number between 0-75 spinnerType int // spinnerTypeOptionUsed remembers if the spinnerType was changed manually spinnerTypeOptionUsed bool // spinner represents the spinner as a slice of string spinner []string // fullWidth specifies whether to measure and set the bar to a specific width fullWidth bool // invisible doesn't render the bar at all, useful for debugging invisible bool onCompletion func() // whether the render function should make use of ANSI codes to reduce console I/O useANSICodes bool // showDescriptionAtLineEnd specifies whether description should be written at line end instead of line start showDescriptionAtLineEnd bool } // Theme defines the elements of the bar type Theme struct { Saucer string AltSaucerHead string SaucerHead string SaucerPadding string BarStart string BarEnd string } // Option is the type all options need to adhere to type Option func(p *ProgressBar) // OptionSetWidth sets the width of the bar func OptionSetWidth(s int) Option { return func(p *ProgressBar) { p.config.width = s } } // OptionSpinnerType sets the type of spinner used for indeterminate bars func OptionSpinnerType(spinnerType int) Option { return func(p *ProgressBar) { p.config.spinnerTypeOptionUsed = true p.config.spinnerType = spinnerType } } // OptionSpinnerCustom sets the spinner used for indeterminate bars to the passed // slice of string func OptionSpinnerCustom(spinner []string) Option { return func(p *ProgressBar) { p.config.spinner = spinner } } // OptionSetTheme sets the elements the bar is constructed of func OptionSetTheme(t Theme) Option { return func(p *ProgressBar) { p.config.theme = t } } // OptionSetVisibility sets the visibility func OptionSetVisibility(visibility bool) Option { return func(p *ProgressBar) { p.config.invisible = !visibility } } // OptionFullWidth sets the bar to be full width func OptionFullWidth() Option { return func(p *ProgressBar) { p.config.fullWidth = true } } // OptionSetWriter sets the output writer (defaults to os.StdOut) func OptionSetWriter(w io.Writer) Option { return func(p *ProgressBar) { p.config.writer = w } } // OptionSetRenderBlankState sets whether or not to render a 0% bar on construction func OptionSetRenderBlankState(r bool) Option { return func(p *ProgressBar) { p.config.renderWithBlankState = r } } // OptionSetDescription sets the description of the bar to render in front of it func OptionSetDescription(description string) Option { return func(p *ProgressBar) { p.config.description = description } } // OptionEnableColorCodes enables or disables support for color codes // using mitchellh/colorstring func OptionEnableColorCodes(colorCodes bool) Option { return func(p *ProgressBar) { p.config.colorCodes = colorCodes } } // OptionSetElapsedTime will enable elapsed time. Always enabled if OptionSetPredictTime is true. func OptionSetElapsedTime(elapsedTime bool) Option { return func(p *ProgressBar) { p.config.elapsedTime = elapsedTime } } // OptionSetPredictTime will also attempt to predict the time remaining. func OptionSetPredictTime(predictTime bool) Option { return func(p *ProgressBar) { p.config.predictTime = predictTime } } // OptionShowCount will also print current count out of total func OptionShowCount() Option { return func(p *ProgressBar) { p.config.showIterationsCount = true } } // OptionShowIts will also print the iterations/second func OptionShowIts() Option { return func(p *ProgressBar) { p.config.showIterationsPerSecond = true } } // OptionShowElapsedOnFinish will keep the display of elapsed time on finish func OptionShowElapsedTimeOnFinish() Option { return func(p *ProgressBar) { p.config.showElapsedTimeOnFinish = true } } // OptionSetItsString sets what's displayed for iterations a second. The default is "it" which would display: "it/s" func OptionSetItsString(iterationString string) Option { return func(p *ProgressBar) { p.config.iterationString = iterationString } } // OptionThrottle will wait the specified duration before updating again. The default // duration is 0 seconds. func OptionThrottle(duration time.Duration) Option { return func(p *ProgressBar) { p.config.throttleDuration = duration } } // OptionClearOnFinish will clear the bar once its finished func OptionClearOnFinish() Option { return func(p *ProgressBar) { p.config.clearOnFinish = true } } // OptionOnCompletion will invoke cmpl function once its finished func OptionOnCompletion(cmpl func()) Option { return func(p *ProgressBar) { p.config.onCompletion = cmpl } } // OptionShowBytes will update the progress bar // configuration settings to display/hide kBytes/Sec func OptionShowBytes(val bool) Option { return func(p *ProgressBar) { p.config.showBytes = val } } // OptionUseANSICodes will use more optimized terminal i/o. // // Only useful in environments with support for ANSI escape sequences. func OptionUseANSICodes(val bool) Option { return func(p *ProgressBar) { p.config.useANSICodes = val } } // OptionShowDescriptionAtLineEnd defines whether description should be written at line end instead of line start func OptionShowDescriptionAtLineEnd() Option { return func(p *ProgressBar) { p.config.showDescriptionAtLineEnd = true } } 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...) } // NewOptions64 constructs a new instance of ProgressBar, with any options you specify func NewOptions64(max int64, options ...Option) *ProgressBar { b := ProgressBar{ state: getBasicState(), config: config{ writer: os.Stdout, theme: defaultTheme, iterationString: "it", width: 40, max: max, throttleDuration: 0 * time.Nanosecond, elapsedTime: true, predictTime: true, spinnerType: 9, invisible: false, }, } for _, o := range options { o(&b) } if b.config.spinnerType < 0 || b.config.spinnerType > 75 { panic("invalid spinner type, must be between 0 and 75") } // ignoreLength if max bytes not known if b.config.max == -1 { b.config.ignoreLength = true b.config.max = int64(b.config.width) b.config.predictTime = false } b.config.maxHumanized, b.config.maxHumanizedSuffix = humanizeBytes(float64(b.config.max)) if b.config.renderWithBlankState { b.RenderBlank() } return &b } func getBasicState() state { now := time.Now() return state{ startTime: now, lastShown: now, counterTime: now, } } // New returns a new ProgressBar // with the specified maximum func New(max int) *ProgressBar { return NewOptions(max) } // DefaultBytes provides a progressbar to measure byte // throughput with recommended defaults. // Set maxBytes to -1 to use as a spinner. func DefaultBytes(maxBytes int64, description ...string) *ProgressBar { desc := "" if len(description) > 0 { desc = description[0] } return NewOptions64( maxBytes, OptionSetDescription(desc), OptionSetWriter(os.Stderr), OptionShowBytes(true), OptionSetWidth(10), OptionThrottle(65*time.Millisecond), OptionShowCount(), OptionOnCompletion(func() { fmt.Fprint(os.Stderr, "\n") }), OptionSpinnerType(14), OptionFullWidth(), OptionSetRenderBlankState(true), ) } // DefaultBytesSilent is the same as DefaultBytes, but does not output anywhere. // String() can be used to get the output instead. func DefaultBytesSilent(maxBytes int64, description ...string) *ProgressBar { // Mostly the same bar as DefaultBytes desc := "" if len(description) > 0 { desc = description[0] } return NewOptions64( maxBytes, OptionSetDescription(desc), OptionSetWriter(io.Discard), OptionShowBytes(true), OptionSetWidth(10), OptionThrottle(65*time.Millisecond), OptionShowCount(), OptionSpinnerType(14), OptionFullWidth(), ) } // Default provides a progressbar with recommended defaults. // Set max to -1 to use as a spinner. func Default(max int64, description ...string) *ProgressBar { desc := "" if len(description) > 0 { desc = description[0] } return NewOptions64( max, OptionSetDescription(desc), OptionSetWriter(os.Stderr), OptionSetWidth(10), OptionThrottle(65*time.Millisecond), OptionShowCount(), OptionShowIts(), OptionOnCompletion(func() { fmt.Fprint(os.Stderr, "\n") }), OptionSpinnerType(14), OptionFullWidth(), OptionSetRenderBlankState(true), ) } // DefaultSilent is the same as Default, but does not output anywhere. // String() can be used to get the output instead. func DefaultSilent(max int64, description ...string) *ProgressBar { // Mostly the same bar as Default desc := "" if len(description) > 0 { desc = description[0] } return NewOptions64( max, OptionSetDescription(desc), OptionSetWriter(io.Discard), OptionSetWidth(10), OptionThrottle(65*time.Millisecond), OptionShowCount(), OptionShowIts(), OptionSpinnerType(14), OptionFullWidth(), ) } // String returns the current rendered version of the progress bar. // It will never return an empty string while the progress bar is running. func (p *ProgressBar) String() string { return p.state.rendered } // RenderBlank renders the current bar state, you can use this to render a 0% state func (p *ProgressBar) RenderBlank() error { if p.config.invisible { return nil } if p.state.currentNum == 0 { p.state.lastShown = time.Time{} } return p.render() } // Reset will reset the clock that is used // to calculate current time and the time left. func (p *ProgressBar) Reset() { p.lock.Lock() defer p.lock.Unlock() p.state = getBasicState() } // Finish will fill the bar to full func (p *ProgressBar) Finish() error { p.lock.Lock() p.state.currentNum = p.config.max p.lock.Unlock() return p.Add(0) } // Exit will exit the bar to keep current state func (p *ProgressBar) Exit() error { p.lock.Lock() defer p.lock.Unlock() p.state.exit = true if p.config.onCompletion != nil { p.config.onCompletion() } return nil } // Add will add the specified amount to the progressbar func (p *ProgressBar) Add(num int) error { return p.Add64(int64(num)) } // Set will set the bar to a current number func (p *ProgressBar) Set(num int) error { return p.Set64(int64(num)) } // Set64 will set the bar to a current number func (p *ProgressBar) Set64(num int64) error { p.lock.Lock() toAdd := num - int64(p.state.currentBytes) p.lock.Unlock() return p.Add64(toAdd) } // Add64 will add the specified amount to the progressbar func (p *ProgressBar) Add64(num int64) error { if p.config.invisible { return nil } p.lock.Lock() defer p.lock.Unlock() if p.state.exit { return nil } // error out since OptionSpinnerCustom will always override a manually set spinnerType if p.config.spinnerTypeOptionUsed && len(p.config.spinner) > 0 { return errors.New("OptionSpinnerType and OptionSpinnerCustom cannot be used together") } if p.config.max == 0 { return errors.New("max must be greater than 0") } if p.state.currentNum < p.config.max { if p.config.ignoreLength { p.state.currentNum = (p.state.currentNum + num) % p.config.max } else { p.state.currentNum += num } } p.state.currentBytes += float64(num) // reset the countdown timer every second to take rolling average p.state.counterNumSinceLast += num if time.Since(p.state.counterTime).Seconds() > 0.5 { p.state.counterLastTenRates = append(p.state.counterLastTenRates, float64(p.state.counterNumSinceLast)/time.Since(p.state.counterTime).Seconds()) if len(p.state.counterLastTenRates) > 10 { p.state.counterLastTenRates = p.state.counterLastTenRates[1:] } p.state.counterTime = time.Now() p.state.counterNumSinceLast = 0 } percent := float64(p.state.currentNum) / float64(p.config.max) p.state.currentSaucerSize = int(percent * float64(p.config.width)) p.state.currentPercent = int(percent * 100) updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0 p.state.lastPercent = p.state.currentPercent if p.state.currentNum > p.config.max { return errors.New("current number exceeds max") } // always update if show bytes/second or its/second if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount { return p.render() } return nil } // Clear erases the progress bar from the current line func (p *ProgressBar) Clear() error { return clearProgressBar(p.config, p.state) } // Describe will change the description shown before the progress, which // can be changed on the fly (as for a slow running process). func (p *ProgressBar) Describe(description string) { p.lock.Lock() defer p.lock.Unlock() p.config.description = description if p.config.invisible { return } p.render() } // New64 returns a new ProgressBar // with the specified maximum func New64(max int64) *ProgressBar { return NewOptions64(max) } // GetMax returns the max of a bar func (p *ProgressBar) GetMax() int { return int(p.config.max) } // GetMax64 returns the current max func (p *ProgressBar) GetMax64() int64 { return p.config.max } // ChangeMax takes in a int // and changes the max value // of the progress bar func (p *ProgressBar) ChangeMax(newMax int) { p.ChangeMax64(int64(newMax)) } // ChangeMax64 is basically // the same as ChangeMax, // but takes in a int64 // to avoid casting func (p *ProgressBar) ChangeMax64(newMax int64) { p.config.max = newMax if p.config.showBytes { p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max)) } p.Add(0) // re-render } // IsFinished returns true if progress bar is completed func (p *ProgressBar) IsFinished() bool { return p.state.finished } // 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() && p.state.currentNum < p.config.max { return nil } if !p.config.useANSICodes { // first, clear the existing progress bar err := clearProgressBar(p.config, p.state) if err != nil { return err } } // check if the progress bar is finished if !p.state.finished && p.state.currentNum >= p.config.max { p.state.finished = true if !p.config.clearOnFinish { renderProgressBar(p.config, &p.state) } if p.config.onCompletion != nil { p.config.onCompletion() } } if p.state.finished { // when using ANSI codes we don't pre-clean the current line if p.config.useANSICodes && p.config.clearOnFinish { err := clearProgressBar(p.config, p.state) if err != nil { return err } } return nil } // then, re-render the current progress bar w, err := renderProgressBar(p.config, &p.state) if err != nil { return err } if w > p.state.maxLineWidth { p.state.maxLineWidth = w } p.state.lastShown = time.Now() return nil } // State returns the current state func (p *ProgressBar) State() State { p.lock.Lock() defer p.lock.Unlock() s := 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.state.currentNum > 0 { s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum)) } s.KBsPerSecond = float64(p.state.currentBytes) / 1000.0 / s.SecondsSince return s } // regex matching ansi escape codes var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) func getStringWidth(c config, str string, colorize bool) int { if c.colorCodes { // convert any color codes in the progress bar into the respective ANSI codes str = colorstring.Color(str) } // the width of the string, if printed to the console // does not include the carriage return character cleanString := strings.Replace(str, "\r", "", -1) if c.colorCodes { // the ANSI codes for the colors do not take up space in the console output, // so they do not count towards the output string width cleanString = ansiRegex.ReplaceAllString(cleanString, "") } // get the amount of runes in the string instead of the // character count of the string, as some runes span multiple characters. // see https://stackoverflow.com/a/12668840/2733724 stringWidth := runewidth.StringWidth(cleanString) return stringWidth } func renderProgressBar(c config, s *state) (int, error) { var sb strings.Builder averageRate := average(s.counterLastTenRates) if len(s.counterLastTenRates) == 0 || s.finished { // if no average samples, or if finished, // then average rate should be the total rate if t := time.Since(s.startTime).Seconds(); t > 0 { averageRate = s.currentBytes / t } else { averageRate = 0 } } // show iteration count in "current/total" iterations format if c.showIterationsCount { if sb.Len() == 0 { sb.WriteString("(") } else { sb.WriteString(", ") } if !c.ignoreLength { if c.showBytes { currentHumanize, currentSuffix := humanizeBytes(s.currentBytes) if currentSuffix == c.maxHumanizedSuffix { sb.WriteString(fmt.Sprintf("%s/%s%s", currentHumanize, c.maxHumanized, c.maxHumanizedSuffix)) } else { sb.WriteString(fmt.Sprintf("%s%s/%s%s", currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix)) } } else { sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max)) } } else { if c.showBytes { currentHumanize, currentSuffix := humanizeBytes(s.currentBytes) sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix)) } else { sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-")) } } } // show rolling average rate if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) { if sb.Len() == 0 { sb.WriteString("(") } else { sb.WriteString(", ") } currentHumanize, currentSuffix := humanizeBytes(averageRate) sb.WriteString(fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix)) } // show iterations rate if c.showIterationsPerSecond { if sb.Len() == 0 { sb.WriteString("(") } else { sb.WriteString(", ") } if averageRate > 1 { sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString)) } else if averageRate*60 > 1 { sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString)) } else { sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString)) } } if sb.Len() > 0 { sb.WriteString(")") } leftBrac, rightBrac, saucer, saucerHead := "", "", "", "" // show time prediction in "current/total" seconds format switch { case c.predictTime: rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second) if rightBracNum.Seconds() < 0 { rightBracNum = 0 * time.Second } rightBrac = rightBracNum.String() fallthrough case c.elapsedTime: leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String() } if c.fullWidth && !c.ignoreLength { width, err := termWidth() if err != nil { width = 80 } amend := 1 // an extra space at eol switch { case leftBrac != "" && rightBrac != "": amend = 4 // space, square brackets and colon case leftBrac != "" && rightBrac == "": amend = 4 // space and square brackets and another space case leftBrac == "" && rightBrac != "": amend = 3 // space and square brackets } if c.showDescriptionAtLineEnd { amend += 1 // another space } 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 { if c.ignoreLength { saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1) } else { saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1) } // Check if an alternate saucer head is set for animation if c.theme.AltSaucerHead != "" && s.isAltSaucerHead { saucerHead = c.theme.AltSaucerHead s.isAltSaucerHead = false } else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width { // use the saucer for the saucer head if it hasn't been set // to preserve backwards compatibility saucerHead = c.theme.Saucer } else { saucerHead = c.theme.SaucerHead s.isAltSaucerHead = true } } /* Progress Bar format Description % |------ | (kb/s) (iteration count) (iteration rate) (predict time) or if showDescriptionAtLineEnd is enabled % |------ | (kb/s) (iteration count) (iteration rate) (predict time) Description */ repeatAmount := c.width - s.currentSaucerSize if repeatAmount < 0 { repeatAmount = 0 } str := "" if c.ignoreLength { selectedSpinner := spinners[c.spinnerType] 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)))))] if c.elapsedTime { if c.showDescriptionAtLineEnd { str = fmt.Sprintf("\r%s %s [%s] %s ", spinner, sb.String(), leftBrac, c.description) } else { str = fmt.Sprintf("\r%s %s %s [%s] ", spinner, c.description, sb.String(), leftBrac) } } else { if c.showDescriptionAtLineEnd { str = fmt.Sprintf("\r%s %s %s ", spinner, sb.String(), c.description) } else { str = fmt.Sprintf("\r%s %s %s ", spinner, c.description, sb.String()) } } } else if rightBrac == "" { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", s.currentPercent, c.theme.BarStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), c.theme.BarEnd, sb.String()) if s.currentPercent == 100 && c.showElapsedTimeOnFinish { str = fmt.Sprintf("%s [%s]", str, leftBrac) } if c.showDescriptionAtLineEnd { str = fmt.Sprintf("\r%s %s ", str, c.description) } else { str = fmt.Sprintf("\r%s%s ", c.description, str) } } else { if s.currentPercent == 100 { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", s.currentPercent, c.theme.BarStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), c.theme.BarEnd, sb.String()) if c.showElapsedTimeOnFinish { str = fmt.Sprintf("%s [%s]", str, leftBrac) } if c.showDescriptionAtLineEnd { str = fmt.Sprintf("\r%s %s", str, c.description) } else { str = fmt.Sprintf("\r%s%s", c.description, str) } } else { str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]", s.currentPercent, c.theme.BarStart, saucer, saucerHead, strings.Repeat(c.theme.SaucerPadding, repeatAmount), c.theme.BarEnd, sb.String(), leftBrac, rightBrac) if c.showDescriptionAtLineEnd { str = fmt.Sprintf("\r%s %s", str, c.description) } else { str = fmt.Sprintf("\r%s%s", c.description, str) } } } if c.colorCodes { // convert any color codes in the progress bar into the respective ANSI codes str = colorstring.Color(str) } s.rendered = str return getStringWidth(c, str, false), writeString(c, str) } func clearProgressBar(c config, s state) error { if s.maxLineWidth == 0 { return nil } if c.useANSICodes { // write the "clear current line" ANSI escape sequence return writeString(c, "\033[2K\r") } // fill the empty content // to overwrite the progress bar and jump // back to the beginning of the line str := fmt.Sprintf("\r%s\r", strings.Repeat(" ", s.maxLineWidth)) return writeString(c, str) // the following does not show correctly if the previous line is longer than subsequent line // return writeString(c, "\r") } func writeString(c config, str string) error { if _, err := io.WriteString(c.writer, str); err != nil { return err } if f, ok := c.writer.(*os.File); ok { // ignore any errors in Sync(), as stdout // can't be synced on some operating systems // like Debian 9 (Stretch) f.Sync() } return nil } // Reader is the progressbar io.Reader struct type Reader struct { io.Reader bar *ProgressBar } // NewReader return a new Reader with a given progress bar. func NewReader(r io.Reader, bar *ProgressBar) Reader { return Reader{ Reader: r, bar: bar, } } // Read will read the data and add the number of bytes to the progressbar func (r *Reader) Read(p []byte) (n int, err error) { n, err = r.Reader.Read(p) r.bar.Add(n) return } // Close the reader when it implements io.Closer func (r *Reader) Close() (err error) { if closer, ok := r.Reader.(io.Closer); ok { return closer.Close() } r.bar.Finish() return } // Write implement io.Writer func (p *ProgressBar) Write(b []byte) (n int, err error) { n = len(b) p.Add(n) return } // Read implement io.Reader func (p *ProgressBar) Read(b []byte) (n int, err error) { n = len(b) p.Add(n) return } func (p *ProgressBar) Close() (err error) { p.Finish() return } func average(xs []float64) float64 { total := 0.0 for _, v := range xs { total += v } return total / float64(len(xs)) } func humanizeBytes(s float64) (string, string) { sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"} base := 1000.0 if s < 10 { return fmt.Sprintf("%2.0f", s), sizes[0] } e := math.Floor(logn(float64(s), base)) suffix := sizes[int(e)] val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 f := "%.0f" if val < 10 { f = "%.1f" } return fmt.Sprintf(f, val), suffix } func logn(n, b float64) float64 { return math.Log(n) / math.Log(b) } // termWidth function returns the visible width of the current terminal // and can be redefined for testing var termWidth = func() (width int, err error) { width, _, err = term.GetSize(int(os.Stdout.Fd())) if err == nil { return width, nil } return 0, err }