1098 lines
28 KiB
Go
1098 lines
28 KiB
Go
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
|
|
}
|