diff --git a/bar.go b/bar.go new file mode 100644 index 0000000..6c27be2 --- /dev/null +++ b/bar.go @@ -0,0 +1,237 @@ +package uiprogress + +import ( + "bytes" + "errors" + "fmt" + "sync" + "time" + + "github.com/gosuri/uiprogress/util/strutil" +) + +var ( + // Fill is the default character representing completed progress + Fill byte = '=' + + // Head is the default character that moves when progress is updated + Head byte = '>' + + // Empty is the default character that represents the empty progress + Empty byte = '-' + + // LeftEnd is the default character in the left most part of the progress indicator + LeftEnd byte = '[' + + // RightEnd is the default character in the right most part of the progress indicator + RightEnd byte = ']' + + // Width is the default width of the progress bar + Width = 70 + + // ErrMaxCurrentReached is error when trying to set current value that exceeds the total value + ErrMaxCurrentReached = errors.New("errors: current value is greater total value") +) + +// Bar represents a progress bar +type Bar struct { + // Total of the total for the progress bar + Total int + + // LeftEnd is character in the left most part of the progress indicator. Defaults to '[' + LeftEnd byte + + // RightEnd is character in the right most part of the progress indicator. Defaults to ']' + RightEnd byte + + // Fill is the character representing completed progress. Defaults to '=' + Fill byte + + // Head is the character that moves when progress is updated. Defaults to '>' + Head byte + + // Empty is the character that represents the empty progress. Default is '-' + Empty byte + + // TimeStated is time progress began + TimeStarted time.Time + + // Width is the width of the progress bar + Width int + + // timeElased is the time elapsed for the progress + timeElapsed time.Duration + current int + + mtx *sync.RWMutex + + appendFuncs []DecoratorFunc + prependFuncs []DecoratorFunc +} + +// DecoratorFunc is a function that can be prepended and appended to the progress bar +type DecoratorFunc func(b *Bar) string + +// NewBar returns a new progress bar +func NewBar(total int) *Bar { + return &Bar{ + Total: total, + Width: Width, + LeftEnd: LeftEnd, + RightEnd: RightEnd, + Head: Head, + Fill: Fill, + Empty: Empty, + + mtx: &sync.RWMutex{}, + } +} + +// Set the current count of the bar. It returns ErrMaxCurrentReached when trying n exceeds the total value. This is atomic operation and concurancy safe. +func (b *Bar) Set(n int) error { + b.mtx.Lock() + defer b.mtx.Unlock() + + if n > b.Total { + return ErrMaxCurrentReached + } + b.current = n + return nil +} + +// Incr increments the current value by 1, time elapsed to current time and returns true. It returns false if the cursor has reached or exceeds total value. +func (b *Bar) Incr() bool { + b.mtx.Lock() + defer b.mtx.Unlock() + + n := b.current + 1 + if n > b.Total { + return false + } + var t time.Time + if b.TimeStarted == t { + b.TimeStarted = time.Now() + } + b.timeElapsed = time.Since(b.TimeStarted) + b.current = n + return true +} + +// Current returns the current progress of the bar +func (b *Bar) Current() int { + b.mtx.RLock() + defer b.mtx.RUnlock() + return b.current +} + +// AppendFunc runs the decorator function and renders the output on the right of the progress bar +func (b *Bar) AppendFunc(f DecoratorFunc) *Bar { + b.mtx.Lock() + defer b.mtx.Unlock() + b.appendFuncs = append(b.appendFuncs, f) + return b +} + +// AppendCompleted appends the completion percent to the progress bar +func (b *Bar) AppendCompleted() *Bar { + b.AppendFunc(func(b *Bar) string { + return b.CompletedPercentString() + }) + return b +} + +// AppendElapsed appends the time elapsed the be progress bar +func (b *Bar) AppendElapsed() *Bar { + b.AppendFunc(func(b *Bar) string { + return strutil.PadLeft(b.TimeElapsedString(), 5, ' ') + }) + return b +} + +// PrependFunc runs decorator function and render the output left the progress bar +func (b *Bar) PrependFunc(f DecoratorFunc) *Bar { + b.mtx.Lock() + defer b.mtx.Unlock() + b.prependFuncs = append(b.prependFuncs, f) + return b +} + +// PrependCompleted prepends the precent completed to the progress bar +func (b *Bar) PrependCompleted() *Bar { + b.PrependFunc(func(b *Bar) string { + return b.CompletedPercentString() + }) + return b +} + +// PrependElapsed prepends the time elapsed to the begining of the bar +func (b *Bar) PrependElapsed() *Bar { + b.PrependFunc(func(b *Bar) string { + return strutil.PadLeft(b.TimeElapsedString(), 5, ' ') + }) + return b +} + +// Bytes returns the byte presentation of the progress bar +func (b *Bar) Bytes() []byte { + completedWidth := int(float64(b.Width) * (b.CompletedPercent() / 100.00)) + + // add fill and empty bits + var buf bytes.Buffer + for i := 0; i < completedWidth; i++ { + buf.WriteByte(b.Fill) + } + for i := 0; i < b.Width-completedWidth; i++ { + buf.WriteByte(b.Empty) + } + + // set head bit + pb := buf.Bytes() + if completedWidth > 0 && completedWidth < b.Width { + pb[completedWidth-1] = b.Head + } + + // set left and right ends bits + pb[0], pb[len(pb)-1] = b.LeftEnd, b.RightEnd + + // render append functions to the right of the bar + for _, f := range b.appendFuncs { + pb = append(pb, ' ') + pb = append(pb, []byte(f(b))...) + } + + // render prepend functions to the left of the bar + for _, f := range b.prependFuncs { + args := []byte(f(b)) + args = append(args, ' ') + pb = append(args, pb...) + } + return pb +} + +// String returns the string representation of the bar +func (b *Bar) String() string { + return string(b.Bytes()) +} + +// CompletedPercent return the percent completed +func (b *Bar) CompletedPercent() float64 { + return (float64(b.Current()) / float64(b.Total)) * 100.00 +} + +// CompletedPercentString returns the formatted string representation of the completed percent +func (b *Bar) CompletedPercentString() string { + return fmt.Sprintf("%3.f%%", b.CompletedPercent()) +} + +// TimeElapsed returns the time elapsed +func (b *Bar) TimeElapsed() time.Duration { + b.mtx.RLock() + defer b.mtx.RUnlock() + return b.timeElapsed +} + +// TimeElapsedString returns the formatted string represenation of the time elapsed +func (b *Bar) TimeElapsedString() string { + return strutil.PrettyTime(b.TimeElapsed()) +} diff --git a/progress.go b/progress.go new file mode 100644 index 0000000..1e7d0be --- /dev/null +++ b/progress.go @@ -0,0 +1,76 @@ +package uiprogress + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/gosuri/uilive" +) + +// Out is the default writer to render progress bars to +var Out = os.Stdout + +// RefreshInterval in the default time duration to wait for refreshing the output +var RefreshInterval = time.Millisecond * 10 + +// Progress represents the container that renders progress bars +type Progress struct { + // Out is the writer to render progress bars to + Out io.Writer + + // Width is the width of the progress bars + Width int + + // Bars is the collection of progress bars + // Bars []*Bar + + // RefreshInterval in the time duration to wait for refreshing the output + RefreshInterval time.Duration + + lw *uilive.Writer + stopChan chan struct{} + // mtx *sync.RWMutex + bars chan Bar + ticker *time.Ticker +} + +// New returns a new progress bar with defaults +func New() *Progress { + return &Progress{ + Width: Width, + Out: Out, + RefreshInterval: RefreshInterval, + + lw: uilive.New(), + stopChan: make(chan struct{}), + bars: make(chan Bar), + } +} + +// Listen listens for updates and renders the progress bars +func (p *Progress) Listen() { + bars := make([]Bar, 0) + p.lw.Out = p.Out +loop: + for { + select { + case <-p.stopChan: // interrupt + return + case bar, ok := <-p.bars: + if !ok { + break loop + } + bars = append(bars, bar) + default: + time.Sleep(p.RefreshInterval) + p.mtx.RLock() + for _, bar := range p.Bars { + fmt.Fprintln(p.lw, bar.String()) + } + p.lw.Flush() + p.mtx.RUnlock() + } + } +}