summaryrefslogtreecommitdiffstats
path: root/decor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:12:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:12:43 +0000
commite28e21a9397e402a78499ceeef63dd19989be29e (patch)
treef4b7f6aaa6bb1563cac5f10b0bde8b6333ac4e37 /decor
parentInitial commit. (diff)
downloadgolang-github-vbauerster-mpb-e28e21a9397e402a78499ceeef63dd19989be29e.tar.xz
golang-github-vbauerster-mpb-e28e21a9397e402a78499ceeef63dd19989be29e.zip
Adding upstream version 8.6.1.upstream/8.6.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'decor')
-rw-r--r--decor/any.go21
-rw-r--r--decor/counters.go253
-rw-r--r--decor/decorator.go186
-rw-r--r--decor/doc.go19
-rw-r--r--decor/elapsed.go33
-rw-r--r--decor/eta.go205
-rw-r--r--decor/meta.go34
-rw-r--r--decor/moving_average.go74
-rw-r--r--decor/name.go11
-rw-r--r--decor/on_abort.go68
-rw-r--r--decor/on_compete_or_on_abort.go21
-rw-r--r--decor/on_complete.go67
-rw-r--r--decor/on_condition.go51
-rw-r--r--decor/percentage.go68
-rw-r--r--decor/percentage_test.go58
-rw-r--r--decor/size_type.go120
-rw-r--r--decor/size_type_test.go160
-rw-r--r--decor/sizeb1000_string.go41
-rw-r--r--decor/sizeb1024_string.go41
-rw-r--r--decor/speed.go179
-rw-r--r--decor/speed_test.go278
-rw-r--r--decor/spinner.go21
22 files changed, 2009 insertions, 0 deletions
diff --git a/decor/any.go b/decor/any.go
new file mode 100644
index 0000000..ca208d8
--- /dev/null
+++ b/decor/any.go
@@ -0,0 +1,21 @@
+package decor
+
+var _ Decorator = any{}
+
+// Any decorator.
+// Converts DecorFunc into Decorator.
+//
+// `fn` DecorFunc callback
+// `wcc` optional WC config
+func Any(fn DecorFunc, wcc ...WC) Decorator {
+ return any{initWC(wcc...), fn}
+}
+
+type any struct {
+ WC
+ fn DecorFunc
+}
+
+func (d any) Decor(s Statistics) (string, int) {
+ return d.Format(d.fn(s))
+}
diff --git a/decor/counters.go b/decor/counters.go
new file mode 100644
index 0000000..0420275
--- /dev/null
+++ b/decor/counters.go
@@ -0,0 +1,253 @@
+package decor
+
+import (
+ "fmt"
+)
+
+// CountersNoUnit is a wrapper around Counters with no unit param.
+func CountersNoUnit(pairFmt string, wcc ...WC) Decorator {
+ return Counters(0, pairFmt, wcc...)
+}
+
+// CountersKibiByte is a wrapper around Counters with predefined unit
+// as SizeB1024(0).
+func CountersKibiByte(pairFmt string, wcc ...WC) Decorator {
+ return Counters(SizeB1024(0), pairFmt, wcc...)
+}
+
+// CountersKiloByte is a wrapper around Counters with predefined unit
+// as SizeB1000(0).
+func CountersKiloByte(pairFmt string, wcc ...WC) Decorator {
+ return Counters(SizeB1000(0), pairFmt, wcc...)
+}
+
+// Counters decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `pairFmt` printf compatible verbs for current and total
+//
+// `wcc` optional WC config
+//
+// pairFmt example if unit=SizeB1000(0):
+//
+// pairFmt="%d / %d" output: "1MB / 12MB"
+// pairFmt="% d / % d" output: "1 MB / 12 MB"
+// pairFmt="%.1f / %.1f" output: "1.0MB / 12.0MB"
+// pairFmt="% .1f / % .1f" output: "1.0 MB / 12.0 MB"
+// pairFmt="%f / %f" output: "1.000000MB / 12.000000MB"
+// pairFmt="% f / % f" output: "1.000000 MB / 12.000000 MB"
+func Counters(unit interface{}, pairFmt string, wcc ...WC) Decorator {
+ producer := func() DecorFunc {
+ switch unit.(type) {
+ case SizeB1024:
+ if pairFmt == "" {
+ pairFmt = "% d / % d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(pairFmt, SizeB1024(s.Current), SizeB1024(s.Total))
+ }
+ case SizeB1000:
+ if pairFmt == "" {
+ pairFmt = "% d / % d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(pairFmt, SizeB1000(s.Current), SizeB1000(s.Total))
+ }
+ default:
+ if pairFmt == "" {
+ pairFmt = "%d / %d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(pairFmt, s.Current, s.Total)
+ }
+ }
+ }
+ return Any(producer(), wcc...)
+}
+
+// TotalNoUnit is a wrapper around Total with no unit param.
+func TotalNoUnit(format string, wcc ...WC) Decorator {
+ return Total(0, format, wcc...)
+}
+
+// TotalKibiByte is a wrapper around Total with predefined unit
+// as SizeB1024(0).
+func TotalKibiByte(format string, wcc ...WC) Decorator {
+ return Total(SizeB1024(0), format, wcc...)
+}
+
+// TotalKiloByte is a wrapper around Total with predefined unit
+// as SizeB1000(0).
+func TotalKiloByte(format string, wcc ...WC) Decorator {
+ return Total(SizeB1000(0), format, wcc...)
+}
+
+// Total decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `format` printf compatible verb for Total
+//
+// `wcc` optional WC config
+//
+// format example if unit=SizeB1024(0):
+//
+// format="%d" output: "12MiB"
+// format="% d" output: "12 MiB"
+// format="%.1f" output: "12.0MiB"
+// format="% .1f" output: "12.0 MiB"
+// format="%f" output: "12.000000MiB"
+// format="% f" output: "12.000000 MiB"
+func Total(unit interface{}, format string, wcc ...WC) Decorator {
+ producer := func() DecorFunc {
+ switch unit.(type) {
+ case SizeB1024:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1024(s.Total))
+ }
+ case SizeB1000:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1000(s.Total))
+ }
+ default:
+ if format == "" {
+ format = "%d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, s.Total)
+ }
+ }
+ }
+ return Any(producer(), wcc...)
+}
+
+// CurrentNoUnit is a wrapper around Current with no unit param.
+func CurrentNoUnit(format string, wcc ...WC) Decorator {
+ return Current(0, format, wcc...)
+}
+
+// CurrentKibiByte is a wrapper around Current with predefined unit
+// as SizeB1024(0).
+func CurrentKibiByte(format string, wcc ...WC) Decorator {
+ return Current(SizeB1024(0), format, wcc...)
+}
+
+// CurrentKiloByte is a wrapper around Current with predefined unit
+// as SizeB1000(0).
+func CurrentKiloByte(format string, wcc ...WC) Decorator {
+ return Current(SizeB1000(0), format, wcc...)
+}
+
+// Current decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `format` printf compatible verb for Current
+//
+// `wcc` optional WC config
+//
+// format example if unit=SizeB1024(0):
+//
+// format="%d" output: "12MiB"
+// format="% d" output: "12 MiB"
+// format="%.1f" output: "12.0MiB"
+// format="% .1f" output: "12.0 MiB"
+// format="%f" output: "12.000000MiB"
+// format="% f" output: "12.000000 MiB"
+func Current(unit interface{}, format string, wcc ...WC) Decorator {
+ producer := func() DecorFunc {
+ switch unit.(type) {
+ case SizeB1024:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1024(s.Current))
+ }
+ case SizeB1000:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1000(s.Current))
+ }
+ default:
+ if format == "" {
+ format = "%d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, s.Current)
+ }
+ }
+ }
+ return Any(producer(), wcc...)
+}
+
+// InvertedCurrentNoUnit is a wrapper around InvertedCurrent with no unit param.
+func InvertedCurrentNoUnit(format string, wcc ...WC) Decorator {
+ return InvertedCurrent(0, format, wcc...)
+}
+
+// InvertedCurrentKibiByte is a wrapper around InvertedCurrent with predefined unit
+// as SizeB1024(0).
+func InvertedCurrentKibiByte(format string, wcc ...WC) Decorator {
+ return InvertedCurrent(SizeB1024(0), format, wcc...)
+}
+
+// InvertedCurrentKiloByte is a wrapper around InvertedCurrent with predefined unit
+// as SizeB1000(0).
+func InvertedCurrentKiloByte(format string, wcc ...WC) Decorator {
+ return InvertedCurrent(SizeB1000(0), format, wcc...)
+}
+
+// InvertedCurrent decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `format` printf compatible verb for InvertedCurrent
+//
+// `wcc` optional WC config
+//
+// format example if unit=SizeB1024(0):
+//
+// format="%d" output: "12MiB"
+// format="% d" output: "12 MiB"
+// format="%.1f" output: "12.0MiB"
+// format="% .1f" output: "12.0 MiB"
+// format="%f" output: "12.000000MiB"
+// format="% f" output: "12.000000 MiB"
+func InvertedCurrent(unit interface{}, format string, wcc ...WC) Decorator {
+ producer := func() DecorFunc {
+ switch unit.(type) {
+ case SizeB1024:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1024(s.Total-s.Current))
+ }
+ case SizeB1000:
+ if format == "" {
+ format = "% d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, SizeB1000(s.Total-s.Current))
+ }
+ default:
+ if format == "" {
+ format = "%d"
+ }
+ return func(s Statistics) string {
+ return fmt.Sprintf(format, s.Total-s.Current)
+ }
+ }
+ }
+ return Any(producer(), wcc...)
+}
diff --git a/decor/decorator.go b/decor/decorator.go
new file mode 100644
index 0000000..f537d3f
--- /dev/null
+++ b/decor/decorator.go
@@ -0,0 +1,186 @@
+package decor
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/mattn/go-runewidth"
+)
+
+const (
+ // DidentRight bit specifies identation direction.
+ //
+ // |foo |b | With DidentRight
+ // | foo| b| Without DidentRight
+ DidentRight = 1 << iota
+
+ // DextraSpace bit adds extra space, makes sense with DSyncWidth only.
+ // When DidentRight bit set, the space will be added to the right,
+ // otherwise to the left.
+ DextraSpace
+
+ // DSyncWidth bit enables same column width synchronization.
+ // Effective with multiple bars only.
+ DSyncWidth
+
+ // DSyncWidthR is shortcut for DSyncWidth|DidentRight
+ DSyncWidthR = DSyncWidth | DidentRight
+
+ // DSyncSpace is shortcut for DSyncWidth|DextraSpace
+ DSyncSpace = DSyncWidth | DextraSpace
+
+ // DSyncSpaceR is shortcut for DSyncWidth|DextraSpace|DidentRight
+ DSyncSpaceR = DSyncWidth | DextraSpace | DidentRight
+)
+
+// TimeStyle enum.
+type TimeStyle int
+
+// TimeStyle kinds.
+const (
+ ET_STYLE_GO TimeStyle = iota
+ ET_STYLE_HHMMSS
+ ET_STYLE_HHMM
+ ET_STYLE_MMSS
+)
+
+// Statistics contains fields which are necessary for implementing
+// `decor.Decorator` and `mpb.BarFiller` interfaces.
+type Statistics struct {
+ AvailableWidth int // calculated width initially equal to terminal width
+ RequestedWidth int // width set by `mpb.WithWidth`
+ ID int
+ Total int64
+ Current int64
+ Refill int64
+ Completed bool
+ Aborted bool
+}
+
+// Decorator interface.
+// Most of the time there is no need to implement this interface
+// manually, as decor package already provides a wide range of decorators
+// which implement this interface. If however built-in decorators don't
+// meet your needs, you're free to implement your own one by implementing
+// this particular interface. The easy way to go is to convert a
+// `DecorFunc` into a `Decorator` interface by using provided
+// `func Any(DecorFunc, ...WC) Decorator`.
+type Decorator interface {
+ Synchronizer
+ Formatter
+ Decor(Statistics) (str string, viewWidth int)
+}
+
+// DecorFunc func type.
+// To be used with `func Any(DecorFunc, ...WC) Decorator`.
+type DecorFunc func(Statistics) string
+
+// Synchronizer interface.
+// All decorators implement this interface implicitly. Its Sync
+// method exposes width sync channel, if DSyncWidth bit is set.
+type Synchronizer interface {
+ Sync() (chan int, bool)
+}
+
+// Formatter interface.
+// Format method needs to be called from within Decorator.Decor method
+// in order to format string according to decor.WC settings.
+// No need to implement manually as long as decor.WC is embedded.
+type Formatter interface {
+ Format(string) (str string, viewWidth int)
+}
+
+// Wrapper interface.
+// If you're implementing custom Decorator by wrapping a built-in one,
+// it is necessary to implement this interface to retain functionality
+// of built-in Decorator.
+type Wrapper interface {
+ Unwrap() Decorator
+}
+
+// EwmaDecorator interface.
+// EWMA based decorators should implement this one.
+type EwmaDecorator interface {
+ EwmaUpdate(int64, time.Duration)
+}
+
+// AverageDecorator interface.
+// Average decorators should implement this interface to provide start
+// time adjustment facility, for resume-able tasks.
+type AverageDecorator interface {
+ AverageAdjust(time.Time)
+}
+
+// ShutdownListener interface.
+// If decorator needs to be notified once upon bar shutdown event, so
+// this is the right interface to implement.
+type ShutdownListener interface {
+ OnShutdown()
+}
+
+// Global convenience instances of WC with sync width bit set.
+// To be used with multiple bars only, i.e. not effective for single bar usage.
+var (
+ WCSyncWidth = WC{C: DSyncWidth}
+ WCSyncWidthR = WC{C: DSyncWidthR}
+ WCSyncSpace = WC{C: DSyncSpace}
+ WCSyncSpaceR = WC{C: DSyncSpaceR}
+)
+
+// WC is a struct with two public fields W and C, both of int type.
+// W represents width and C represents bit set of width related config.
+// A decorator should embed WC, to enable width synchronization.
+type WC struct {
+ W int
+ C int
+ fill func(s string, w int) string
+ wsync chan int
+}
+
+// Format should be called by any Decorator implementation.
+// Returns formatted string and its view (visual) width.
+func (wc WC) Format(str string) (string, int) {
+ viewWidth := runewidth.StringWidth(str)
+ if wc.W > viewWidth {
+ viewWidth = wc.W
+ }
+ if (wc.C & DSyncWidth) != 0 {
+ if (wc.C & DextraSpace) != 0 {
+ viewWidth++
+ }
+ wc.wsync <- viewWidth
+ viewWidth = <-wc.wsync
+ }
+ return wc.fill(str, viewWidth), viewWidth
+}
+
+// Init initializes width related config.
+func (wc *WC) Init() WC {
+ if (wc.C & DidentRight) != 0 {
+ wc.fill = runewidth.FillRight
+ } else {
+ wc.fill = runewidth.FillLeft
+ }
+ if (wc.C & DSyncWidth) != 0 {
+ // it's deliberate choice to override wsync on each Init() call,
+ // this way globals like WCSyncSpace can be reused
+ wc.wsync = make(chan int)
+ }
+ return *wc
+}
+
+// Sync is implementation of Synchronizer interface.
+func (wc WC) Sync() (chan int, bool) {
+ if (wc.C&DSyncWidth) != 0 && wc.wsync == nil {
+ panic(fmt.Sprintf("%T is not initialized", wc))
+ }
+ return wc.wsync, (wc.C & DSyncWidth) != 0
+}
+
+func initWC(wcc ...WC) WC {
+ var wc WC
+ for _, nwc := range wcc {
+ wc = nwc
+ }
+ return wc.Init()
+}
diff --git a/decor/doc.go b/decor/doc.go
new file mode 100644
index 0000000..d41aa50
--- /dev/null
+++ b/decor/doc.go
@@ -0,0 +1,19 @@
+// Package decor provides common decorators for "github.com/vbauerster/mpb/v8" module.
+//
+// Some decorators returned by this package might have a closure state. It is ok to use
+// decorators concurrently, unless you share the same decorator among multiple
+// *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance.
+//
+// Don't:
+//
+// p := mpb.New()
+// name := decor.Name("bar")
+// p.AddBar(100, mpb.AppendDecorators(name))
+// p.AddBar(100, mpb.AppendDecorators(name))
+//
+// Do:
+//
+// p := mpb.New()
+// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1")))
+// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2")))
+package decor
diff --git a/decor/elapsed.go b/decor/elapsed.go
new file mode 100644
index 0000000..6cee7d1
--- /dev/null
+++ b/decor/elapsed.go
@@ -0,0 +1,33 @@
+package decor
+
+import (
+ "time"
+)
+
+// Elapsed decorator. It's wrapper of NewElapsed.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `wcc` optional WC config
+func Elapsed(style TimeStyle, wcc ...WC) Decorator {
+ return NewElapsed(style, time.Now(), wcc...)
+}
+
+// NewElapsed returns elapsed time decorator.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `startTime` start time
+//
+// `wcc` optional WC config
+func NewElapsed(style TimeStyle, startTime time.Time, wcc ...WC) Decorator {
+ var msg string
+ producer := chooseTimeProducer(style)
+ fn := func(s Statistics) string {
+ if !s.Completed {
+ msg = producer(time.Since(startTime))
+ }
+ return msg
+ }
+ return Any(fn, wcc...)
+}
diff --git a/decor/eta.go b/decor/eta.go
new file mode 100644
index 0000000..ecb6f8f
--- /dev/null
+++ b/decor/eta.go
@@ -0,0 +1,205 @@
+package decor
+
+import (
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/VividCortex/ewma"
+)
+
+var (
+ _ Decorator = (*movingAverageETA)(nil)
+ _ EwmaDecorator = (*movingAverageETA)(nil)
+ _ Decorator = (*averageETA)(nil)
+ _ AverageDecorator = (*averageETA)(nil)
+)
+
+// TimeNormalizer interface. Implementors could be passed into
+// MovingAverageETA, in order to affect i.e. normalize its output.
+type TimeNormalizer interface {
+ Normalize(time.Duration) time.Duration
+}
+
+// TimeNormalizerFunc is function type adapter to convert function
+// into TimeNormalizer.
+type TimeNormalizerFunc func(time.Duration) time.Duration
+
+func (f TimeNormalizerFunc) Normalize(src time.Duration) time.Duration {
+ return f(src)
+}
+
+// EwmaETA exponential-weighted-moving-average based ETA decorator. For this
+// decorator to work correctly you have to measure each iteration's duration
+// and pass it to one of the (*Bar).EwmaIncr... family methods.
+func EwmaETA(style TimeStyle, age float64, wcc ...WC) Decorator {
+ var average ewma.MovingAverage
+ if age == 0 {
+ average = ewma.NewMovingAverage()
+ } else {
+ average = ewma.NewMovingAverage(age)
+ }
+ return MovingAverageETA(style, NewThreadSafeMovingAverage(average), nil, wcc...)
+}
+
+// MovingAverageETA decorator relies on MovingAverage implementation to calculate its average.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `average` implementation of MovingAverage interface
+//
+// `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer]
+//
+// `wcc` optional WC config
+func MovingAverageETA(style TimeStyle, average ewma.MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator {
+ d := &movingAverageETA{
+ WC: initWC(wcc...),
+ average: average,
+ normalizer: normalizer,
+ producer: chooseTimeProducer(style),
+ }
+ return d
+}
+
+type movingAverageETA struct {
+ WC
+ average ewma.MovingAverage
+ normalizer TimeNormalizer
+ producer func(time.Duration) string
+}
+
+func (d *movingAverageETA) Decor(s Statistics) (string, int) {
+ v := math.Round(d.average.Value())
+ remaining := time.Duration((s.Total - s.Current) * int64(v))
+ if d.normalizer != nil {
+ remaining = d.normalizer.Normalize(remaining)
+ }
+ return d.Format(d.producer(remaining))
+}
+
+func (d *movingAverageETA) EwmaUpdate(n int64, dur time.Duration) {
+ durPerItem := float64(dur) / float64(n)
+ if math.IsInf(durPerItem, 0) || math.IsNaN(durPerItem) {
+ return
+ }
+ d.average.Add(durPerItem)
+}
+
+// AverageETA decorator. It's wrapper of NewAverageETA.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `wcc` optional WC config
+func AverageETA(style TimeStyle, wcc ...WC) Decorator {
+ return NewAverageETA(style, time.Now(), nil, wcc...)
+}
+
+// NewAverageETA decorator with user provided start time.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `startTime` start time
+//
+// `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer]
+//
+// `wcc` optional WC config
+func NewAverageETA(style TimeStyle, startTime time.Time, normalizer TimeNormalizer, wcc ...WC) Decorator {
+ d := &averageETA{
+ WC: initWC(wcc...),
+ startTime: startTime,
+ normalizer: normalizer,
+ producer: chooseTimeProducer(style),
+ }
+ return d
+}
+
+type averageETA struct {
+ WC
+ startTime time.Time
+ normalizer TimeNormalizer
+ producer func(time.Duration) string
+}
+
+func (d *averageETA) Decor(s Statistics) (string, int) {
+ var remaining time.Duration
+ if s.Current != 0 {
+ durPerItem := float64(time.Since(d.startTime)) / float64(s.Current)
+ durPerItem = math.Round(durPerItem)
+ remaining = time.Duration((s.Total - s.Current) * int64(durPerItem))
+ if d.normalizer != nil {
+ remaining = d.normalizer.Normalize(remaining)
+ }
+ }
+ return d.Format(d.producer(remaining))
+}
+
+func (d *averageETA) AverageAdjust(startTime time.Time) {
+ d.startTime = startTime
+}
+
+// MaxTolerateTimeNormalizer returns implementation of TimeNormalizer.
+func MaxTolerateTimeNormalizer(maxTolerate time.Duration) TimeNormalizer {
+ var normalized time.Duration
+ var lastCall time.Time
+ return TimeNormalizerFunc(func(remaining time.Duration) time.Duration {
+ if diff := normalized - remaining; diff <= 0 || diff > maxTolerate || remaining < time.Minute {
+ normalized = remaining
+ lastCall = time.Now()
+ return remaining
+ }
+ normalized -= time.Since(lastCall)
+ lastCall = time.Now()
+ return normalized
+ })
+}
+
+// FixedIntervalTimeNormalizer returns implementation of TimeNormalizer.
+func FixedIntervalTimeNormalizer(updInterval int) TimeNormalizer {
+ var normalized time.Duration
+ var lastCall time.Time
+ var count int
+ return TimeNormalizerFunc(func(remaining time.Duration) time.Duration {
+ if count == 0 || remaining < time.Minute {
+ count = updInterval
+ normalized = remaining
+ lastCall = time.Now()
+ return remaining
+ }
+ count--
+ normalized -= time.Since(lastCall)
+ lastCall = time.Now()
+ return normalized
+ })
+}
+
+func chooseTimeProducer(style TimeStyle) func(time.Duration) string {
+ switch style {
+ case ET_STYLE_HHMMSS:
+ return func(remaining time.Duration) string {
+ hours := int64(remaining/time.Hour) % 60
+ minutes := int64(remaining/time.Minute) % 60
+ seconds := int64(remaining/time.Second) % 60
+ return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ }
+ case ET_STYLE_HHMM:
+ return func(remaining time.Duration) string {
+ hours := int64(remaining/time.Hour) % 60
+ minutes := int64(remaining/time.Minute) % 60
+ return fmt.Sprintf("%02d:%02d", hours, minutes)
+ }
+ case ET_STYLE_MMSS:
+ return func(remaining time.Duration) string {
+ hours := int64(remaining/time.Hour) % 60
+ minutes := int64(remaining/time.Minute) % 60
+ seconds := int64(remaining/time.Second) % 60
+ if hours > 0 {
+ return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ }
+ return fmt.Sprintf("%02d:%02d", minutes, seconds)
+ }
+ default:
+ return func(remaining time.Duration) string {
+ return remaining.Truncate(time.Second).String()
+ }
+ }
+}
diff --git a/decor/meta.go b/decor/meta.go
new file mode 100644
index 0000000..0045a31
--- /dev/null
+++ b/decor/meta.go
@@ -0,0 +1,34 @@
+package decor
+
+var (
+ _ Decorator = metaWrapper{}
+ _ Wrapper = metaWrapper{}
+)
+
+// Meta wrap decorator.
+// Provided fn is supposed to wrap output of given decorator
+// with meta information like ANSI escape codes for example.
+// Primary usage intention is to set SGR display attributes.
+//
+// `decorator` Decorator to wrap
+// `fn` func to apply meta information
+func Meta(decorator Decorator, fn func(string) string) Decorator {
+ if decorator == nil {
+ return nil
+ }
+ return metaWrapper{decorator, fn}
+}
+
+type metaWrapper struct {
+ Decorator
+ fn func(string) string
+}
+
+func (d metaWrapper) Decor(s Statistics) (string, int) {
+ str, width := d.Decorator.Decor(s)
+ return d.fn(str), width
+}
+
+func (d metaWrapper) Unwrap() Decorator {
+ return d.Decorator
+}
diff --git a/decor/moving_average.go b/decor/moving_average.go
new file mode 100644
index 0000000..a1be8ad
--- /dev/null
+++ b/decor/moving_average.go
@@ -0,0 +1,74 @@
+package decor
+
+import (
+ "sort"
+ "sync"
+
+ "github.com/VividCortex/ewma"
+)
+
+var (
+ _ ewma.MovingAverage = (*threadSafeMovingAverage)(nil)
+ _ ewma.MovingAverage = (*medianWindow)(nil)
+ _ sort.Interface = (*medianWindow)(nil)
+)
+
+type threadSafeMovingAverage struct {
+ ewma.MovingAverage
+ mu sync.Mutex
+}
+
+func (s *threadSafeMovingAverage) Add(value float64) {
+ s.mu.Lock()
+ s.MovingAverage.Add(value)
+ s.mu.Unlock()
+}
+
+func (s *threadSafeMovingAverage) Value() float64 {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.MovingAverage.Value()
+}
+
+func (s *threadSafeMovingAverage) Set(value float64) {
+ s.mu.Lock()
+ s.MovingAverage.Set(value)
+ s.mu.Unlock()
+}
+
+// NewThreadSafeMovingAverage converts provided ewma.MovingAverage
+// into thread safe ewma.MovingAverage.
+func NewThreadSafeMovingAverage(average ewma.MovingAverage) ewma.MovingAverage {
+ if tsma, ok := average.(*threadSafeMovingAverage); ok {
+ return tsma
+ }
+ return &threadSafeMovingAverage{MovingAverage: average}
+}
+
+type medianWindow [3]float64
+
+func (s *medianWindow) Len() int { return len(s) }
+func (s *medianWindow) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s *medianWindow) Less(i, j int) bool { return s[i] < s[j] }
+
+func (s *medianWindow) Add(value float64) {
+ s[0], s[1] = s[1], s[2]
+ s[2] = value
+}
+
+func (s *medianWindow) Value() float64 {
+ tmp := *s
+ sort.Sort(&tmp)
+ return tmp[1]
+}
+
+func (s *medianWindow) Set(value float64) {
+ for i := 0; i < len(s); i++ {
+ s[i] = value
+ }
+}
+
+// NewMedian is fixed last 3 samples median MovingAverage.
+func NewMedian() ewma.MovingAverage {
+ return NewThreadSafeMovingAverage(new(medianWindow))
+}
diff --git a/decor/name.go b/decor/name.go
new file mode 100644
index 0000000..31ac123
--- /dev/null
+++ b/decor/name.go
@@ -0,0 +1,11 @@
+package decor
+
+// Name decorator displays text that is set once and can't be changed
+// during decorator's lifetime.
+//
+// `str` string to display
+//
+// `wcc` optional WC config
+func Name(str string, wcc ...WC) Decorator {
+ return Any(func(Statistics) string { return str }, wcc...)
+}
diff --git a/decor/on_abort.go b/decor/on_abort.go
new file mode 100644
index 0000000..50a1dfb
--- /dev/null
+++ b/decor/on_abort.go
@@ -0,0 +1,68 @@
+package decor
+
+var (
+ _ Decorator = onAbortWrapper{}
+ _ Wrapper = onAbortWrapper{}
+ _ Decorator = onAbortMetaWrapper{}
+ _ Wrapper = onAbortMetaWrapper{}
+)
+
+// OnAbort wrap decorator.
+// Displays provided message on abort event.
+// Has no effect if bar.Abort(true) is called.
+//
+// `decorator` Decorator to wrap
+// `message` message to display
+func OnAbort(decorator Decorator, message string) Decorator {
+ if decorator == nil {
+ return nil
+ }
+ return onAbortWrapper{decorator, message}
+}
+
+type onAbortWrapper struct {
+ Decorator
+ msg string
+}
+
+func (d onAbortWrapper) Decor(s Statistics) (string, int) {
+ if s.Aborted {
+ return d.Format(d.msg)
+ }
+ return d.Decorator.Decor(s)
+}
+
+func (d onAbortWrapper) Unwrap() Decorator {
+ return d.Decorator
+}
+
+// OnAbortMeta wrap decorator.
+// Provided fn is supposed to wrap output of given decorator
+// with meta information like ANSI escape codes for example.
+// Primary usage intention is to set SGR display attributes.
+//
+// `decorator` Decorator to wrap
+// `fn` func to apply meta information
+func OnAbortMeta(decorator Decorator, fn func(string) string) Decorator {
+ if decorator == nil {
+ return nil
+ }
+ return onAbortMetaWrapper{decorator, fn}
+}
+
+type onAbortMetaWrapper struct {
+ Decorator
+ fn func(string) string
+}
+
+func (d onAbortMetaWrapper) Decor(s Statistics) (string, int) {
+ if s.Completed {
+ str, width := d.Decorator.Decor(s)
+ return d.fn(str), width
+ }
+ return d.Decorator.Decor(s)
+}
+
+func (d onAbortMetaWrapper) Unwrap() Decorator {
+ return d.Decorator
+}
diff --git a/decor/on_compete_or_on_abort.go b/decor/on_compete_or_on_abort.go
new file mode 100644
index 0000000..f9ca841
--- /dev/null
+++ b/decor/on_compete_or_on_abort.go
@@ -0,0 +1,21 @@
+package decor
+
+// OnCompleteOrOnAbort wrap decorator.
+// Displays provided message on complete or on abort event.
+//
+// `decorator` Decorator to wrap
+// `message` message to display
+func OnCompleteOrOnAbort(decorator Decorator, message string) Decorator {
+ return OnComplete(OnAbort(decorator, message), message)
+}
+
+// OnCompleteMetaOrOnAbortMeta wrap decorator.
+// Provided fn is supposed to wrap output of given decorator
+// with meta information like ANSI escape codes for example.
+// Primary usage intention is to set SGR display attributes.
+//
+// `decorator` Decorator to wrap
+// `fn` func to apply meta information
+func OnCompleteMetaOrOnAbortMeta(decorator Decorator, fn func(string) string) Decorator {
+ return OnCompleteMeta(OnAbortMeta(decorator, fn), fn)
+}
diff --git a/decor/on_complete.go b/decor/on_complete.go
new file mode 100644
index 0000000..f18b5a6
--- /dev/null
+++ b/decor/on_complete.go
@@ -0,0 +1,67 @@
+package decor
+
+var (
+ _ Decorator = onCompleteWrapper{}
+ _ Wrapper = onCompleteWrapper{}
+ _ Decorator = onCompleteMetaWrapper{}
+ _ Wrapper = onCompleteMetaWrapper{}
+)
+
+// OnComplete wrap decorator.
+// Displays provided message on complete event.
+//
+// `decorator` Decorator to wrap
+// `message` message to display
+func OnComplete(decorator Decorator, message string) Decorator {
+ if decorator == nil {
+ return nil
+ }
+ return onCompleteWrapper{decorator, message}
+}
+
+type onCompleteWrapper struct {
+ Decorator
+ msg string
+}
+
+func (d onCompleteWrapper) Decor(s Statistics) (string, int) {
+ if s.Completed {
+ return d.Format(d.msg)
+ }
+ return d.Decorator.Decor(s)
+}
+
+func (d onCompleteWrapper) Unwrap() Decorator {
+ return d.Decorator
+}
+
+// OnCompleteMeta wrap decorator.
+// Provided fn is supposed to wrap output of given decorator
+// with meta information like ANSI escape codes for example.
+// Primary usage intention is to set SGR display attributes.
+//
+// `decorator` Decorator to wrap
+// `fn` func to apply meta information
+func OnCompleteMeta(decorator Decorator, fn func(string) string) Decorator {
+ if decorator == nil {
+ return nil
+ }
+ return onCompleteMetaWrapper{decorator, fn}
+}
+
+type onCompleteMetaWrapper struct {
+ Decorator
+ fn func(string) string
+}
+
+func (d onCompleteMetaWrapper) Decor(s Statistics) (string, int) {
+ if s.Completed {
+ str, width := d.Decorator.Decor(s)
+ return d.fn(str), width
+ }
+ return d.Decorator.Decor(s)
+}
+
+func (d onCompleteMetaWrapper) Unwrap() Decorator {
+ return d.Decorator
+}
diff --git a/decor/on_condition.go b/decor/on_condition.go
new file mode 100644
index 0000000..f4626c3
--- /dev/null
+++ b/decor/on_condition.go
@@ -0,0 +1,51 @@
+package decor
+
+// OnCondition applies decorator only if a condition is true.
+//
+// `decorator` Decorator
+//
+// `cond` bool
+func OnCondition(decorator Decorator, cond bool) Decorator {
+ return Conditional(cond, decorator, nil)
+}
+
+// OnPredicate applies decorator only if a predicate evaluates to true.
+//
+// `decorator` Decorator
+//
+// `predicate` func() bool
+func OnPredicate(decorator Decorator, predicate func() bool) Decorator {
+ return Predicative(predicate, decorator, nil)
+}
+
+// Conditional returns decorator `a` if condition is true, otherwise
+// decorator `b`.
+//
+// `cond` bool
+//
+// `a` Decorator
+//
+// `b` Decorator
+func Conditional(cond bool, a, b Decorator) Decorator {
+ if cond {
+ return a
+ } else {
+ return b
+ }
+}
+
+// Predicative returns decorator `a` if predicate evaluates to true,
+// otherwise decorator `b`.
+//
+// `predicate` func() bool
+//
+// `a` Decorator
+//
+// `b` Decorator
+func Predicative(predicate func() bool, a, b Decorator) Decorator {
+ if predicate() {
+ return a
+ } else {
+ return b
+ }
+}
diff --git a/decor/percentage.go b/decor/percentage.go
new file mode 100644
index 0000000..9709c19
--- /dev/null
+++ b/decor/percentage.go
@@ -0,0 +1,68 @@
+package decor
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/vbauerster/mpb/v8/internal"
+)
+
+var _ fmt.Formatter = percentageType(0)
+
+type percentageType float64
+
+func (s percentageType) Format(st fmt.State, verb rune) {
+ prec := -1
+ switch verb {
+ case 'f', 'e', 'E':
+ prec = 6 // default prec of fmt.Printf("%f|%e|%E")
+ fallthrough
+ case 'b', 'g', 'G', 'x', 'X':
+ if p, ok := st.Precision(); ok {
+ prec = p
+ }
+ default:
+ verb, prec = 'f', 0
+ }
+
+ b := strconv.AppendFloat(make([]byte, 0, 16), float64(s), byte(verb), prec, 64)
+ if st.Flag(' ') {
+ b = append(b, ' ', '%')
+ } else {
+ b = append(b, '%')
+ }
+ _, err := st.Write(b)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Percentage returns percentage decorator. It's a wrapper of NewPercentage.
+func Percentage(wcc ...WC) Decorator {
+ return NewPercentage("% d", wcc...)
+}
+
+// NewPercentage percentage decorator with custom format string.
+//
+// `format` printf compatible verb
+//
+// `wcc` optional WC config
+//
+// format examples:
+//
+// format="%d" output: "1%"
+// format="% d" output: "1 %"
+// format="%.1f" output: "1.0%"
+// format="% .1f" output: "1.0 %"
+// format="%f" output: "1.000000%"
+// format="% f" output: "1.000000 %"
+func NewPercentage(format string, wcc ...WC) Decorator {
+ if format == "" {
+ format = "% d"
+ }
+ f := func(s Statistics) string {
+ p := internal.Percentage(s.Total, s.Current, 100)
+ return fmt.Sprintf(format, percentageType(p))
+ }
+ return Any(f, wcc...)
+}
diff --git a/decor/percentage_test.go b/decor/percentage_test.go
new file mode 100644
index 0000000..bbddf5b
--- /dev/null
+++ b/decor/percentage_test.go
@@ -0,0 +1,58 @@
+package decor
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestPercentageType(t *testing.T) {
+ cases := map[string]struct {
+ value float64
+ verb string
+ expected string
+ }{
+ "10 %d": {10, "%d", "10%"},
+ "10 %s": {10, "%s", "10%"},
+ "10 %f": {10, "%f", "10.000000%"},
+ "10 %.6f": {10, "%.6f", "10.000000%"},
+ "10 %.0f": {10, "%.0f", "10%"},
+ "10 %.1f": {10, "%.1f", "10.0%"},
+ "10 %.2f": {10, "%.2f", "10.00%"},
+ "10 %.3f": {10, "%.3f", "10.000%"},
+
+ "10 % d": {10, "% d", "10 %"},
+ "10 % s": {10, "% s", "10 %"},
+ "10 % f": {10, "% f", "10.000000 %"},
+ "10 % .6f": {10, "% .6f", "10.000000 %"},
+ "10 % .0f": {10, "% .0f", "10 %"},
+ "10 % .1f": {10, "% .1f", "10.0 %"},
+ "10 % .2f": {10, "% .2f", "10.00 %"},
+ "10 % .3f": {10, "% .3f", "10.000 %"},
+
+ "10.5 %d": {10.5, "%d", "10%"},
+ "10.5 %s": {10.5, "%s", "10%"},
+ "10.5 %f": {10.5, "%f", "10.500000%"},
+ "10.5 %.6f": {10.5, "%.6f", "10.500000%"},
+ "10.5 %.0f": {10.5, "%.0f", "10%"},
+ "10.5 %.1f": {10.5, "%.1f", "10.5%"},
+ "10.5 %.2f": {10.5, "%.2f", "10.50%"},
+ "10.5 %.3f": {10.5, "%.3f", "10.500%"},
+
+ "10.5 % d": {10.5, "% d", "10 %"},
+ "10.5 % s": {10.5, "% s", "10 %"},
+ "10.5 % f": {10.5, "% f", "10.500000 %"},
+ "10.5 % .6f": {10.5, "% .6f", "10.500000 %"},
+ "10.5 % .0f": {10.5, "% .0f", "10 %"},
+ "10.5 % .1f": {10.5, "% .1f", "10.5 %"},
+ "10.5 % .2f": {10.5, "% .2f", "10.50 %"},
+ "10.5 % .3f": {10.5, "% .3f", "10.500 %"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, percentageType(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
diff --git a/decor/size_type.go b/decor/size_type.go
new file mode 100644
index 0000000..d9950b6
--- /dev/null
+++ b/decor/size_type.go
@@ -0,0 +1,120 @@
+package decor
+
+import (
+ "fmt"
+ "strconv"
+)
+
+//go:generate stringer -type=SizeB1024 -trimprefix=_i
+//go:generate stringer -type=SizeB1000 -trimprefix=_
+
+var (
+ _ fmt.Formatter = SizeB1024(0)
+ _ fmt.Stringer = SizeB1024(0)
+ _ fmt.Formatter = SizeB1000(0)
+ _ fmt.Stringer = SizeB1000(0)
+)
+
+const (
+ _ib SizeB1024 = iota + 1
+ _iKiB SizeB1024 = 1 << (iota * 10)
+ _iMiB
+ _iGiB
+ _iTiB
+)
+
+// SizeB1024 named type, which implements fmt.Formatter interface. It
+// adjusts its value according to byte size multiple by 1024 and appends
+// appropriate size marker (KiB, MiB, GiB, TiB).
+type SizeB1024 int64
+
+func (self SizeB1024) Format(st fmt.State, verb rune) {
+ prec := -1
+ switch verb {
+ case 'f', 'e', 'E':
+ prec = 6 // default prec of fmt.Printf("%f|%e|%E")
+ fallthrough
+ case 'b', 'g', 'G', 'x', 'X':
+ if p, ok := st.Precision(); ok {
+ prec = p
+ }
+ default:
+ verb, prec = 'f', 0
+ }
+
+ var unit SizeB1024
+ switch {
+ case self < _iKiB:
+ unit = _ib
+ case self < _iMiB:
+ unit = _iKiB
+ case self < _iGiB:
+ unit = _iMiB
+ case self < _iTiB:
+ unit = _iGiB
+ default:
+ unit = _iTiB
+ }
+
+ b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64)
+ if st.Flag(' ') {
+ b = append(b, ' ')
+ }
+ b = append(b, []byte(unit.String())...)
+ _, err := st.Write(b)
+ if err != nil {
+ panic(err)
+ }
+}
+
+const (
+ _b SizeB1000 = 1
+ _KB SizeB1000 = _b * 1000
+ _MB SizeB1000 = _KB * 1000
+ _GB SizeB1000 = _MB * 1000
+ _TB SizeB1000 = _GB * 1000
+)
+
+// SizeB1000 named type, which implements fmt.Formatter interface. It
+// adjusts its value according to byte size multiple by 1000 and appends
+// appropriate size marker (KB, MB, GB, TB).
+type SizeB1000 int64
+
+func (self SizeB1000) Format(st fmt.State, verb rune) {
+ prec := -1
+ switch verb {
+ case 'f', 'e', 'E':
+ prec = 6 // default prec of fmt.Printf("%f|%e|%E")
+ fallthrough
+ case 'b', 'g', 'G', 'x', 'X':
+ if p, ok := st.Precision(); ok {
+ prec = p
+ }
+ default:
+ verb, prec = 'f', 0
+ }
+
+ var unit SizeB1000
+ switch {
+ case self < _KB:
+ unit = _b
+ case self < _MB:
+ unit = _KB
+ case self < _GB:
+ unit = _MB
+ case self < _TB:
+ unit = _GB
+ default:
+ unit = _TB
+ }
+
+ b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64)
+ if st.Flag(' ') {
+ b = append(b, ' ')
+ }
+ b = append(b, []byte(unit.String())...)
+ _, err := st.Write(b)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/decor/size_type_test.go b/decor/size_type_test.go
new file mode 100644
index 0000000..117d9fb
--- /dev/null
+++ b/decor/size_type_test.go
@@ -0,0 +1,160 @@
+package decor
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestB1024(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %d": {12345678, "%d", "12MiB"},
+ "verb %s": {12345678, "%s", "12MiB"},
+ "verb %f": {12345678, "%f", "11.773756MiB"},
+ "verb %.6f": {12345678, "%.6f", "11.773756MiB"},
+ "verb %.0f": {12345678, "%.0f", "12MiB"},
+ "verb %.1f": {12345678, "%.1f", "11.8MiB"},
+ "verb %.2f": {12345678, "%.2f", "11.77MiB"},
+ "verb %.3f": {12345678, "%.3f", "11.774MiB"},
+ "verb % d": {12345678, "% d", "12 MiB"},
+ "verb % s": {12345678, "% s", "12 MiB"},
+ "verb % f": {12345678, "% f", "11.773756 MiB"},
+ "verb % .6f": {12345678, "% .6f", "11.773756 MiB"},
+ "verb % .0f": {12345678, "% .0f", "12 MiB"},
+ "verb % .1f": {12345678, "% .1f", "11.8 MiB"},
+ "verb % .2f": {12345678, "% .2f", "11.77 MiB"},
+ "verb % .3f": {12345678, "% .3f", "11.774 MiB"},
+
+ "1000 %d": {1000, "%d", "1000b"},
+ "1000 %s": {1000, "%s", "1000b"},
+ "1000 %f": {1000, "%f", "1000.000000b"},
+ "1000 %.6f": {1000, "%.6f", "1000.000000b"},
+ "1000 %.0f": {1000, "%.0f", "1000b"},
+ "1000 %.1f": {1000, "%.1f", "1000.0b"},
+ "1000 %.2f": {1000, "%.2f", "1000.00b"},
+ "1000 %.3f": {1000, "%.3f", "1000.000b"},
+ "1024 %d": {1024, "%d", "1KiB"},
+ "1024 %s": {1024, "%s", "1KiB"},
+ "1024 %f": {1024, "%f", "1.000000KiB"},
+ "1024 %.6f": {1024, "%.6f", "1.000000KiB"},
+ "1024 %.0f": {1024, "%.0f", "1KiB"},
+ "1024 %.1f": {1024, "%.1f", "1.0KiB"},
+ "1024 %.2f": {1024, "%.2f", "1.00KiB"},
+ "1024 %.3f": {1024, "%.3f", "1.000KiB"},
+
+ "3*MiB+100KiB %d": {3*int64(_iMiB) + 100*int64(_iKiB), "%d", "3MiB"},
+ "3*MiB+100KiB %s": {3*int64(_iMiB) + 100*int64(_iKiB), "%s", "3MiB"},
+ "3*MiB+100KiB %f": {3*int64(_iMiB) + 100*int64(_iKiB), "%f", "3.097656MiB"},
+ "3*MiB+100KiB %.6f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.6f", "3.097656MiB"},
+ "3*MiB+100KiB %.0f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.0f", "3MiB"},
+ "3*MiB+100KiB %.1f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.1f", "3.1MiB"},
+ "3*MiB+100KiB %.2f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.2f", "3.10MiB"},
+ "3*MiB+100KiB %.3f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.3f", "3.098MiB"},
+
+ "2*GiB %d": {2 * int64(_iGiB), "%d", "2GiB"},
+ "2*GiB %s": {2 * int64(_iGiB), "%s", "2GiB"},
+ "2*GiB %f": {2 * int64(_iGiB), "%f", "2.000000GiB"},
+ "2*GiB %.6f": {2 * int64(_iGiB), "%.6f", "2.000000GiB"},
+ "2*GiB %.0f": {2 * int64(_iGiB), "%.0f", "2GiB"},
+ "2*GiB %.1f": {2 * int64(_iGiB), "%.1f", "2.0GiB"},
+ "2*GiB %.2f": {2 * int64(_iGiB), "%.2f", "2.00GiB"},
+ "2*GiB %.3f": {2 * int64(_iGiB), "%.3f", "2.000GiB"},
+
+ "4*TiB %d": {4 * int64(_iTiB), "%d", "4TiB"},
+ "4*TiB %s": {4 * int64(_iTiB), "%s", "4TiB"},
+ "4*TiB %f": {4 * int64(_iTiB), "%f", "4.000000TiB"},
+ "4*TiB %.6f": {4 * int64(_iTiB), "%.6f", "4.000000TiB"},
+ "4*TiB %.0f": {4 * int64(_iTiB), "%.0f", "4TiB"},
+ "4*TiB %.1f": {4 * int64(_iTiB), "%.1f", "4.0TiB"},
+ "4*TiB %.2f": {4 * int64(_iTiB), "%.2f", "4.00TiB"},
+ "4*TiB %.3f": {4 * int64(_iTiB), "%.3f", "4.000TiB"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, SizeB1024(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
+
+func TestB1000(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %d": {12345678, "%d", "12MB"},
+ "verb %s": {12345678, "%s", "12MB"},
+ "verb %f": {12345678, "%f", "12.345678MB"},
+ "verb %.6f": {12345678, "%.6f", "12.345678MB"},
+ "verb %.0f": {12345678, "%.0f", "12MB"},
+ "verb %.1f": {12345678, "%.1f", "12.3MB"},
+ "verb %.2f": {12345678, "%.2f", "12.35MB"},
+ "verb %.3f": {12345678, "%.3f", "12.346MB"},
+ "verb % d": {12345678, "% d", "12 MB"},
+ "verb % s": {12345678, "% s", "12 MB"},
+ "verb % f": {12345678, "% f", "12.345678 MB"},
+ "verb % .6f": {12345678, "% .6f", "12.345678 MB"},
+ "verb % .0f": {12345678, "% .0f", "12 MB"},
+ "verb % .1f": {12345678, "% .1f", "12.3 MB"},
+ "verb % .2f": {12345678, "% .2f", "12.35 MB"},
+ "verb % .3f": {12345678, "% .3f", "12.346 MB"},
+
+ "1000 %d": {1000, "%d", "1KB"},
+ "1000 %s": {1000, "%s", "1KB"},
+ "1000 %f": {1000, "%f", "1.000000KB"},
+ "1000 %.6f": {1000, "%.6f", "1.000000KB"},
+ "1000 %.0f": {1000, "%.0f", "1KB"},
+ "1000 %.1f": {1000, "%.1f", "1.0KB"},
+ "1000 %.2f": {1000, "%.2f", "1.00KB"},
+ "1000 %.3f": {1000, "%.3f", "1.000KB"},
+ "1024 %d": {1024, "%d", "1KB"},
+ "1024 %s": {1024, "%s", "1KB"},
+ "1024 %f": {1024, "%f", "1.024000KB"},
+ "1024 %.6f": {1024, "%.6f", "1.024000KB"},
+ "1024 %.0f": {1024, "%.0f", "1KB"},
+ "1024 %.1f": {1024, "%.1f", "1.0KB"},
+ "1024 %.2f": {1024, "%.2f", "1.02KB"},
+ "1024 %.3f": {1024, "%.3f", "1.024KB"},
+
+ "3*MB+100*KB %d": {3*int64(_MB) + 100*int64(_KB), "%d", "3MB"},
+ "3*MB+100*KB %s": {3*int64(_MB) + 100*int64(_KB), "%s", "3MB"},
+ "3*MB+100*KB %f": {3*int64(_MB) + 100*int64(_KB), "%f", "3.100000MB"},
+ "3*MB+100*KB %.6f": {3*int64(_MB) + 100*int64(_KB), "%.6f", "3.100000MB"},
+ "3*MB+100*KB %.0f": {3*int64(_MB) + 100*int64(_KB), "%.0f", "3MB"},
+ "3*MB+100*KB %.1f": {3*int64(_MB) + 100*int64(_KB), "%.1f", "3.1MB"},
+ "3*MB+100*KB %.2f": {3*int64(_MB) + 100*int64(_KB), "%.2f", "3.10MB"},
+ "3*MB+100*KB %.3f": {3*int64(_MB) + 100*int64(_KB), "%.3f", "3.100MB"},
+
+ "2*GB %d": {2 * int64(_GB), "%d", "2GB"},
+ "2*GB %s": {2 * int64(_GB), "%s", "2GB"},
+ "2*GB %f": {2 * int64(_GB), "%f", "2.000000GB"},
+ "2*GB %.6f": {2 * int64(_GB), "%.6f", "2.000000GB"},
+ "2*GB %.0f": {2 * int64(_GB), "%.0f", "2GB"},
+ "2*GB %.1f": {2 * int64(_GB), "%.1f", "2.0GB"},
+ "2*GB %.2f": {2 * int64(_GB), "%.2f", "2.00GB"},
+ "2*GB %.3f": {2 * int64(_GB), "%.3f", "2.000GB"},
+
+ "4*TB %d": {4 * int64(_TB), "%d", "4TB"},
+ "4*TB %s": {4 * int64(_TB), "%s", "4TB"},
+ "4*TB %f": {4 * int64(_TB), "%f", "4.000000TB"},
+ "4*TB %.6f": {4 * int64(_TB), "%.6f", "4.000000TB"},
+ "4*TB %.0f": {4 * int64(_TB), "%.0f", "4TB"},
+ "4*TB %.1f": {4 * int64(_TB), "%.1f", "4.0TB"},
+ "4*TB %.2f": {4 * int64(_TB), "%.2f", "4.00TB"},
+ "4*TB %.3f": {4 * int64(_TB), "%.3f", "4.000TB"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, SizeB1000(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
diff --git a/decor/sizeb1000_string.go b/decor/sizeb1000_string.go
new file mode 100644
index 0000000..3f32ef7
--- /dev/null
+++ b/decor/sizeb1000_string.go
@@ -0,0 +1,41 @@
+// Code generated by "stringer -type=SizeB1000 -trimprefix=_"; DO NOT EDIT.
+
+package decor
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[_b-1]
+ _ = x[_KB-1000]
+ _ = x[_MB-1000000]
+ _ = x[_GB-1000000000]
+ _ = x[_TB-1000000000000]
+}
+
+const (
+ _SizeB1000_name_0 = "b"
+ _SizeB1000_name_1 = "KB"
+ _SizeB1000_name_2 = "MB"
+ _SizeB1000_name_3 = "GB"
+ _SizeB1000_name_4 = "TB"
+)
+
+func (i SizeB1000) String() string {
+ switch {
+ case i == 1:
+ return _SizeB1000_name_0
+ case i == 1000:
+ return _SizeB1000_name_1
+ case i == 1000000:
+ return _SizeB1000_name_2
+ case i == 1000000000:
+ return _SizeB1000_name_3
+ case i == 1000000000000:
+ return _SizeB1000_name_4
+ default:
+ return "SizeB1000(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+}
diff --git a/decor/sizeb1024_string.go b/decor/sizeb1024_string.go
new file mode 100644
index 0000000..9fca66c
--- /dev/null
+++ b/decor/sizeb1024_string.go
@@ -0,0 +1,41 @@
+// Code generated by "stringer -type=SizeB1024 -trimprefix=_i"; DO NOT EDIT.
+
+package decor
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[_ib-1]
+ _ = x[_iKiB-1024]
+ _ = x[_iMiB-1048576]
+ _ = x[_iGiB-1073741824]
+ _ = x[_iTiB-1099511627776]
+}
+
+const (
+ _SizeB1024_name_0 = "b"
+ _SizeB1024_name_1 = "KiB"
+ _SizeB1024_name_2 = "MiB"
+ _SizeB1024_name_3 = "GiB"
+ _SizeB1024_name_4 = "TiB"
+)
+
+func (i SizeB1024) String() string {
+ switch {
+ case i == 1:
+ return _SizeB1024_name_0
+ case i == 1024:
+ return _SizeB1024_name_1
+ case i == 1048576:
+ return _SizeB1024_name_2
+ case i == 1073741824:
+ return _SizeB1024_name_3
+ case i == 1099511627776:
+ return _SizeB1024_name_4
+ default:
+ return "SizeB1024(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+}
diff --git a/decor/speed.go b/decor/speed.go
new file mode 100644
index 0000000..5879d06
--- /dev/null
+++ b/decor/speed.go
@@ -0,0 +1,179 @@
+package decor
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "time"
+
+ "github.com/VividCortex/ewma"
+)
+
+var (
+ _ Decorator = (*movingAverageSpeed)(nil)
+ _ EwmaDecorator = (*movingAverageSpeed)(nil)
+ _ Decorator = (*averageSpeed)(nil)
+ _ AverageDecorator = (*averageSpeed)(nil)
+)
+
+// FmtAsSpeed adds "/s" to the end of the input formatter. To be
+// used with SizeB1000 or SizeB1024 types, for example:
+//
+// fmt.Printf("%.1f", FmtAsSpeed(SizeB1024(2048)))
+func FmtAsSpeed(input fmt.Formatter) fmt.Formatter {
+ return speedFormatter{input}
+}
+
+type speedFormatter struct {
+ fmt.Formatter
+}
+
+func (self speedFormatter) Format(st fmt.State, verb rune) {
+ self.Formatter.Format(st, verb)
+ _, err := io.WriteString(st, "/s")
+ if err != nil {
+ panic(err)
+ }
+}
+
+// EwmaSpeed exponential-weighted-moving-average based speed decorator.
+// For this decorator to work correctly you have to measure each iteration's
+// duration and pass it to one of the (*Bar).EwmaIncr... family methods.
+func EwmaSpeed(unit interface{}, format string, age float64, wcc ...WC) Decorator {
+ var average ewma.MovingAverage
+ if age == 0 {
+ average = ewma.NewMovingAverage()
+ } else {
+ average = ewma.NewMovingAverage(age)
+ }
+ return MovingAverageSpeed(unit, format, NewThreadSafeMovingAverage(average), wcc...)
+}
+
+// MovingAverageSpeed decorator relies on MovingAverage implementation
+// to calculate its average.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `format` printf compatible verb for value, like "%f" or "%d"
+//
+// `average` MovingAverage implementation
+//
+// `wcc` optional WC config
+//
+// format examples:
+//
+// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s"
+// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s"
+// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s"
+// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s"
+func MovingAverageSpeed(unit interface{}, format string, average ewma.MovingAverage, wcc ...WC) Decorator {
+ d := &movingAverageSpeed{
+ WC: initWC(wcc...),
+ average: average,
+ producer: chooseSpeedProducer(unit, format),
+ }
+ return d
+}
+
+type movingAverageSpeed struct {
+ WC
+ producer func(float64) string
+ average ewma.MovingAverage
+ msg string
+}
+
+func (d *movingAverageSpeed) Decor(s Statistics) (string, int) {
+ if !s.Completed {
+ var speed float64
+ if v := d.average.Value(); v > 0 {
+ speed = 1 / v
+ }
+ d.msg = d.producer(speed * 1e9)
+ }
+ return d.Format(d.msg)
+}
+
+func (d *movingAverageSpeed) EwmaUpdate(n int64, dur time.Duration) {
+ durPerByte := float64(dur) / float64(n)
+ if math.IsInf(durPerByte, 0) || math.IsNaN(durPerByte) {
+ return
+ }
+ d.average.Add(durPerByte)
+}
+
+// AverageSpeed decorator with dynamic unit measure adjustment. It's
+// a wrapper of NewAverageSpeed.
+func AverageSpeed(unit interface{}, format string, wcc ...WC) Decorator {
+ return NewAverageSpeed(unit, format, time.Now(), wcc...)
+}
+
+// NewAverageSpeed decorator with dynamic unit measure adjustment and
+// user provided start time.
+//
+// `unit` one of [0|SizeB1024(0)|SizeB1000(0)]
+//
+// `format` printf compatible verb for value, like "%f" or "%d"
+//
+// `startTime` start time
+//
+// `wcc` optional WC config
+//
+// format examples:
+//
+// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s"
+// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s"
+// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s"
+// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s"
+func NewAverageSpeed(unit interface{}, format string, startTime time.Time, wcc ...WC) Decorator {
+ d := &averageSpeed{
+ WC: initWC(wcc...),
+ startTime: startTime,
+ producer: chooseSpeedProducer(unit, format),
+ }
+ return d
+}
+
+type averageSpeed struct {
+ WC
+ startTime time.Time
+ producer func(float64) string
+ msg string
+}
+
+func (d *averageSpeed) Decor(s Statistics) (string, int) {
+ if !s.Completed {
+ speed := float64(s.Current) / float64(time.Since(d.startTime))
+ d.msg = d.producer(speed * 1e9)
+ }
+ return d.Format(d.msg)
+}
+
+func (d *averageSpeed) AverageAdjust(startTime time.Time) {
+ d.startTime = startTime
+}
+
+func chooseSpeedProducer(unit interface{}, format string) func(float64) string {
+ switch unit.(type) {
+ case SizeB1024:
+ if format == "" {
+ format = "% d"
+ }
+ return func(speed float64) string {
+ return fmt.Sprintf(format, FmtAsSpeed(SizeB1024(math.Round(speed))))
+ }
+ case SizeB1000:
+ if format == "" {
+ format = "% d"
+ }
+ return func(speed float64) string {
+ return fmt.Sprintf(format, FmtAsSpeed(SizeB1000(math.Round(speed))))
+ }
+ default:
+ if format == "" {
+ format = "%f"
+ }
+ return func(speed float64) string {
+ return fmt.Sprintf(format, speed)
+ }
+ }
+}
diff --git a/decor/speed_test.go b/decor/speed_test.go
new file mode 100644
index 0000000..2fe770b
--- /dev/null
+++ b/decor/speed_test.go
@@ -0,0 +1,278 @@
+package decor
+
+import (
+ "testing"
+ "time"
+)
+
+func TestAverageSpeedSizeB1024(t *testing.T) {
+ cases := []struct {
+ name string
+ fmt string
+ unit interface{}
+ current int64
+ elapsed time.Duration
+ expected string
+ }{
+ {
+ name: "empty fmt",
+ unit: SizeB1024(0),
+ fmt: "",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0 b/s",
+ },
+ {
+ name: "SizeB1024(0):%d:0b",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0b/s",
+ },
+ {
+ name: "SizeB1024(0):%f:0b",
+ unit: SizeB1024(0),
+ fmt: "%f",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0.000000b/s",
+ },
+ {
+ name: "SizeB1024(0):% .2f:0b",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0.00 b/s",
+ },
+ {
+ name: "SizeB1024(0):%d:1b",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 1,
+ elapsed: time.Second,
+ expected: "1b/s",
+ },
+ {
+ name: "SizeB1024(0):% .2f:1b",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 1,
+ elapsed: time.Second,
+ expected: "1.00 b/s",
+ },
+ {
+ name: "SizeB1024(0):%d:KiB",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 2 * int64(_iKiB),
+ elapsed: 1 * time.Second,
+ expected: "2KiB/s",
+ },
+ {
+ name: "SizeB1024(0):% .f:KiB",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 2 * int64(_iKiB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 KiB/s",
+ },
+ {
+ name: "SizeB1024(0):%d:MiB",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 2 * int64(_iMiB),
+ elapsed: 1 * time.Second,
+ expected: "2MiB/s",
+ },
+ {
+ name: "SizeB1024(0):% .2f:MiB",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 2 * int64(_iMiB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 MiB/s",
+ },
+ {
+ name: "SizeB1024(0):%d:GiB",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 2 * int64(_iGiB),
+ elapsed: 1 * time.Second,
+ expected: "2GiB/s",
+ },
+ {
+ name: "SizeB1024(0):% .2f:GiB",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 2 * int64(_iGiB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 GiB/s",
+ },
+ {
+ name: "SizeB1024(0):%d:TiB",
+ unit: SizeB1024(0),
+ fmt: "%d",
+ current: 2 * int64(_iTiB),
+ elapsed: 1 * time.Second,
+ expected: "2TiB/s",
+ },
+ {
+ name: "SizeB1024(0):% .2f:TiB",
+ unit: SizeB1024(0),
+ fmt: "% .2f",
+ current: 2 * int64(_iTiB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 TiB/s",
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed))
+ stat := Statistics{
+ Current: tc.current,
+ }
+ res, _ := decor.Decor(stat)
+ if res != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, res)
+ }
+ })
+ }
+}
+
+func TestAverageSpeedSizeB1000(t *testing.T) {
+ cases := []struct {
+ name string
+ fmt string
+ unit interface{}
+ current int64
+ elapsed time.Duration
+ expected string
+ }{
+ {
+ name: "empty fmt",
+ unit: SizeB1000(0),
+ fmt: "",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0 b/s",
+ },
+ {
+ name: "SizeB1000(0):%d:0b",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0b/s",
+ },
+ {
+ name: "SizeB1000(0):%f:0b",
+ unit: SizeB1000(0),
+ fmt: "%f",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0.000000b/s",
+ },
+ {
+ name: "SizeB1000(0):% .2f:0b",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 0,
+ elapsed: time.Second,
+ expected: "0.00 b/s",
+ },
+ {
+ name: "SizeB1000(0):%d:1b",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 1,
+ elapsed: time.Second,
+ expected: "1b/s",
+ },
+ {
+ name: "SizeB1000(0):% .2f:1b",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 1,
+ elapsed: time.Second,
+ expected: "1.00 b/s",
+ },
+ {
+ name: "SizeB1000(0):%d:KB",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 2 * int64(_KB),
+ elapsed: 1 * time.Second,
+ expected: "2KB/s",
+ },
+ {
+ name: "SizeB1000(0):% .f:KB",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 2 * int64(_KB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 KB/s",
+ },
+ {
+ name: "SizeB1000(0):%d:MB",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 2 * int64(_MB),
+ elapsed: 1 * time.Second,
+ expected: "2MB/s",
+ },
+ {
+ name: "SizeB1000(0):% .2f:MB",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 2 * int64(_MB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 MB/s",
+ },
+ {
+ name: "SizeB1000(0):%d:GB",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 2 * int64(_GB),
+ elapsed: 1 * time.Second,
+ expected: "2GB/s",
+ },
+ {
+ name: "SizeB1000(0):% .2f:GB",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 2 * int64(_GB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 GB/s",
+ },
+ {
+ name: "SizeB1000(0):%d:TB",
+ unit: SizeB1000(0),
+ fmt: "%d",
+ current: 2 * int64(_TB),
+ elapsed: 1 * time.Second,
+ expected: "2TB/s",
+ },
+ {
+ name: "SizeB1000(0):% .2f:TB",
+ unit: SizeB1000(0),
+ fmt: "% .2f",
+ current: 2 * int64(_TB),
+ elapsed: 1 * time.Second,
+ expected: "2.00 TB/s",
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed))
+ stat := Statistics{
+ Current: tc.current,
+ }
+ res, _ := decor.Decor(stat)
+ if res != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, res)
+ }
+ })
+ }
+}
diff --git a/decor/spinner.go b/decor/spinner.go
new file mode 100644
index 0000000..9d2f890
--- /dev/null
+++ b/decor/spinner.go
@@ -0,0 +1,21 @@
+package decor
+
+var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
+
+// Spinner returns spinner decorator.
+//
+// `frames` spinner frames, if nil or len==0, default is used
+//
+// `wcc` optional WC config
+func Spinner(frames []string, wcc ...WC) Decorator {
+ if len(frames) == 0 {
+ frames = defaultSpinnerStyle[:]
+ }
+ var count uint
+ f := func(s Statistics) string {
+ frame := frames[count%uint(len(frames))]
+ count++
+ return frame
+ }
+ return Any(f, wcc...)
+}