123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098 |
- 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
- }
|