pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/progress/progress.go (about)

     1  // Package progress provides methods and structs for creating terminal progress bar
     2  package progress
     3  
     4  // ////////////////////////////////////////////////////////////////////////////////// //
     5  //                                                                                    //
     6  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     7  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     8  //                                                                                    //
     9  // ////////////////////////////////////////////////////////////////////////////////// //
    10  
    11  import (
    12  	"fmt"
    13  	"io"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"time"
    19  
    20  	"pkg.re/essentialkaos/ek.v12/fmtc"
    21  	"pkg.re/essentialkaos/ek.v12/fmtutil"
    22  	"pkg.re/essentialkaos/ek.v12/mathutil"
    23  )
    24  
    25  // ////////////////////////////////////////////////////////////////////////////////// //
    26  
    27  // MIN_WIDTH is minimal progress bar width
    28  const MIN_WIDTH = 80
    29  
    30  // PROGRESS_BAR_SYMBOL is symbol for creating progress bar
    31  const PROGRESS_BAR_SYMBOL = "—"
    32  
    33  // ////////////////////////////////////////////////////////////////////////////////// //
    34  
    35  // Bar is progress bar struct
    36  type Bar struct {
    37  	settings Settings
    38  
    39  	startTime time.Time
    40  	started   bool
    41  	finished  bool
    42  
    43  	finishChan chan bool
    44  
    45  	current int64
    46  	total   int64
    47  	name    string
    48  
    49  	buffer string
    50  
    51  	ticker       *time.Ticker
    52  	passThruCalc *PassThruCalc
    53  	phCounter    int
    54  
    55  	reader *passThruReader
    56  	writer *passThruWriter
    57  
    58  	mu *sync.RWMutex
    59  }
    60  
    61  // Settings contains progress bar settings
    62  type Settings struct {
    63  	RefreshRate time.Duration
    64  
    65  	NameColorTag      string
    66  	BarFgColorTag     string
    67  	BarBgColorTag     string
    68  	PercentColorTag   string
    69  	ProgressColorTag  string
    70  	SpeedColorTag     string
    71  	RemainingColorTag string
    72  
    73  	ShowSpeed      bool
    74  	ShowName       bool
    75  	ShowPercentage bool
    76  	ShowProgress   bool
    77  	ShowRemaining  bool
    78  
    79  	Width    int
    80  	NameSize int
    81  
    82  	IsSize bool
    83  }
    84  
    85  // ////////////////////////////////////////////////////////////////////////////////// //
    86  
    87  type passThruReader struct {
    88  	io.Reader
    89  	bar *Bar
    90  }
    91  
    92  type passThruWriter struct {
    93  	io.Writer
    94  	bar *Bar
    95  }
    96  
    97  // ////////////////////////////////////////////////////////////////////////////////// //
    98  
    99  // DefaultSettings is default progress bar settings
   100  var DefaultSettings = Settings{
   101  	RefreshRate:       100 * time.Millisecond,
   102  	NameColorTag:      "{b}",
   103  	BarFgColorTag:     "{r}",
   104  	BarBgColorTag:     "{s-}",
   105  	PercentColorTag:   "{m}",
   106  	SpeedColorTag:     "{r}",
   107  	ProgressColorTag:  "{g}",
   108  	RemainingColorTag: "{c}",
   109  	ShowName:          true,
   110  	ShowPercentage:    true,
   111  	ShowProgress:      true,
   112  	ShowSpeed:         true,
   113  	ShowRemaining:     true,
   114  	IsSize:            true,
   115  	Width:             88,
   116  }
   117  
   118  // ////////////////////////////////////////////////////////////////////////////////// //
   119  
   120  // New creates new progress bar struct
   121  func New(total int64, name string) *Bar {
   122  	return &Bar{
   123  		settings: DefaultSettings,
   124  		name:     name,
   125  		total:    total,
   126  		mu:       &sync.RWMutex{},
   127  	}
   128  }
   129  
   130  // ////////////////////////////////////////////////////////////////////////////////// //
   131  
   132  // Start starts progress processing
   133  func (b *Bar) Start() {
   134  	if b.IsStarted() && !b.IsFinished() {
   135  		return
   136  	}
   137  
   138  	b.phCounter = 0
   139  	b.current = 0
   140  	b.started = true
   141  	b.finished = false
   142  	b.startTime = time.Now()
   143  	b.ticker = time.NewTicker(b.settings.RefreshRate)
   144  	b.finishChan = make(chan bool)
   145  
   146  	if b.total > 0 {
   147  		b.passThruCalc = NewPassThruCalc(b.total, 10.0)
   148  	}
   149  
   150  	go b.renderer()
   151  }
   152  
   153  // Finish finishes progress processing
   154  func (b *Bar) Finish() {
   155  	b.mu.Lock()
   156  	defer b.mu.Unlock()
   157  
   158  	if b.finished || !b.started {
   159  		return
   160  	}
   161  
   162  	fmtc.TPrintf(b.renderElements())
   163  	fmtc.NewLine()
   164  
   165  	b.finishChan <- true
   166  }
   167  
   168  // UpdateSettings updates progress settings
   169  func (b *Bar) UpdateSettings(s Settings) {
   170  	b.mu.Lock()
   171  	b.settings = s
   172  	b.mu.Unlock()
   173  }
   174  
   175  // SetName sets progress bar name
   176  func (b *Bar) SetName(name string) {
   177  	b.mu.Lock()
   178  	b.name = name
   179  	b.mu.Unlock()
   180  }
   181  
   182  // Name returns progress bar name
   183  func (b *Bar) Name() string {
   184  	b.mu.RLock()
   185  	defer b.mu.RUnlock()
   186  	return b.name
   187  }
   188  
   189  // SetTotal sets total progress bar value
   190  func (b *Bar) SetTotal(v int64) {
   191  	b.mu.Lock()
   192  
   193  	if b.passThruCalc == nil {
   194  		b.passThruCalc = NewPassThruCalc(v, 10.0)
   195  	} else {
   196  		b.passThruCalc.SetTotal(v)
   197  	}
   198  
   199  	b.mu.Unlock()
   200  
   201  	atomic.StoreInt64(&b.total, v)
   202  }
   203  
   204  // Total returns total progress bar value
   205  func (b *Bar) Total() int64 {
   206  	return atomic.LoadInt64(&b.total)
   207  }
   208  
   209  // SetCurrent sets current progress bar value
   210  func (b *Bar) SetCurrent(v int64) {
   211  	atomic.StoreInt64(&b.current, v)
   212  }
   213  
   214  // Current returns current progress bar value
   215  func (b *Bar) Current() int64 {
   216  	return atomic.LoadInt64(&b.current)
   217  }
   218  
   219  func (b *Bar) Add(v int) {
   220  	atomic.AddInt64(&b.current, int64(v))
   221  }
   222  
   223  // Add64 adds given value ti
   224  func (b *Bar) Add64(v int64) {
   225  	atomic.AddInt64(&b.current, v)
   226  }
   227  
   228  // IsFinished returns true if progress proccesing is finished
   229  func (b *Bar) IsFinished() bool {
   230  	b.mu.RLock()
   231  	defer b.mu.RUnlock()
   232  	return b.finished
   233  }
   234  
   235  // IsStarted returns true if progress proccesing is started
   236  func (b *Bar) IsStarted() bool {
   237  	b.mu.RLock()
   238  	defer b.mu.RUnlock()
   239  	return b.started
   240  }
   241  
   242  // Reader creates and returns pass thru proxy reader
   243  func (b *Bar) Reader(r io.Reader) io.Reader {
   244  	if b.reader != nil {
   245  		b.reader.Reader = r
   246  	} else {
   247  		b.reader = &passThruReader{
   248  			Reader: r,
   249  			bar:    b,
   250  		}
   251  	}
   252  
   253  	return b.reader
   254  }
   255  
   256  // Writer creates and returns pass thru proxy reader
   257  func (b *Bar) Writer(w io.Writer) io.Writer {
   258  	if b.writer != nil {
   259  		b.writer.Writer = w
   260  	} else {
   261  		b.writer = &passThruWriter{
   262  			Writer: w,
   263  			bar:    b,
   264  		}
   265  	}
   266  
   267  	return b.writer
   268  }
   269  
   270  // ////////////////////////////////////////////////////////////////////////////////// //
   271  
   272  // renderer is rendering loop func
   273  func (b *Bar) renderer() {
   274  	for {
   275  		select {
   276  		case <-b.finishChan:
   277  			b.finished = true
   278  			b.ticker.Stop()
   279  			b.render()
   280  			return
   281  		case <-b.ticker.C:
   282  			b.render()
   283  		}
   284  	}
   285  }
   286  
   287  // render renders current progress bar state
   288  func (b *Bar) render() {
   289  	b.mu.RLock()
   290  	defer b.mu.RUnlock()
   291  
   292  	if !b.finished && b.total > 0 && b.current >= b.total {
   293  		b.finished = true
   294  		b.ticker.Stop()
   295  	}
   296  
   297  	result := b.renderElements()
   298  
   299  	// render text only if changed
   300  	if b.buffer != result {
   301  		fmtc.TPrintf(result)
   302  	}
   303  
   304  	if b.total > 0 {
   305  		b.buffer = result
   306  	}
   307  
   308  	if b.finished {
   309  		fmtc.NewLine()
   310  	}
   311  }
   312  
   313  // renderElements returns text with all progress bar graphics and text
   314  func (b *Bar) renderElements() string {
   315  	var size, totalSize int
   316  	var name, percentage, bar, progress, speed, remaining string
   317  	var statSpeed float64
   318  	var statRemaining time.Duration
   319  
   320  	if b.passThruCalc != nil && (b.settings.ShowSpeed || b.settings.ShowRemaining) {
   321  		if b.finished {
   322  			statRemaining = time.Since(b.startTime)
   323  			statSpeed = float64(b.current) / statRemaining.Seconds()
   324  		} else {
   325  			statSpeed, statRemaining = b.passThruCalc.Calculate(b.current)
   326  		}
   327  	}
   328  
   329  	if b.settings.ShowName && b.name != "" {
   330  		name, size = b.renderName()
   331  		totalSize += size + 1
   332  	}
   333  
   334  	if b.total > 0 {
   335  		if b.settings.ShowPercentage {
   336  			percentage, size = b.renderPercentage()
   337  			totalSize += size + 1
   338  		}
   339  
   340  		if b.settings.ShowProgress {
   341  			progress, size = b.renderProgress()
   342  			totalSize += size + 3
   343  		}
   344  
   345  		if b.settings.ShowSpeed {
   346  			speed, size = b.renderSpeed(statSpeed)
   347  			totalSize += size + 3
   348  		}
   349  
   350  		if b.settings.ShowRemaining {
   351  			remaining, size = b.renderRemaining(statRemaining)
   352  			totalSize += size + 3
   353  		}
   354  	}
   355  
   356  	bar = b.renderBar(totalSize)
   357  
   358  	var result string
   359  
   360  	if b.settings.ShowName && name != "" {
   361  		result += name + " "
   362  	}
   363  
   364  	result += bar + " "
   365  
   366  	if b.total > 0 {
   367  		if b.settings.ShowPercentage {
   368  			result += percentage
   369  		}
   370  
   371  		if b.settings.ShowProgress {
   372  			result += " {s-}•{!} " + progress
   373  		}
   374  
   375  		if b.settings.ShowSpeed {
   376  			result += " {s-}•{!} " + speed
   377  		}
   378  
   379  		if b.settings.ShowRemaining {
   380  			result += " {s-}•{!} " + remaining
   381  		}
   382  	}
   383  
   384  	return result
   385  }
   386  
   387  // renderName returns name text
   388  func (b *Bar) renderName() (string, int) {
   389  	var result string
   390  
   391  	if b.settings.NameSize > 0 && len(b.name) < b.settings.NameSize {
   392  		result = fmt.Sprintf("%"+strconv.Itoa(b.settings.NameSize)+"s", b.name)
   393  	} else {
   394  		result = b.name
   395  	}
   396  
   397  	if fmtc.DisableColors || b.settings.NameColorTag == "" {
   398  		return result, len(result)
   399  	}
   400  
   401  	return b.settings.NameColorTag + result + "{!}", len(result)
   402  }
   403  
   404  // renderPercentage returns parcentage text
   405  func (b *Bar) renderPercentage() (string, int) {
   406  	var perc float64
   407  	var result string
   408  
   409  	switch {
   410  	case b.total <= 0:
   411  		perc = 0.0
   412  	case b.current > b.total:
   413  		perc = 100.0
   414  	default:
   415  		perc = (float64(b.current) / float64(b.total)) * 100.0
   416  	}
   417  
   418  	if perc == 100.0 {
   419  		result = "100%%"
   420  	} else {
   421  		result = fmt.Sprintf("%5.1f", perc) + "%%"
   422  	}
   423  
   424  	if fmtc.DisableColors || b.settings.PercentColorTag == "" {
   425  		return result, len(result)
   426  	}
   427  
   428  	return b.settings.PercentColorTag + result + "{!}", len(result)
   429  }
   430  
   431  // renderProgress returns progress text
   432  func (b *Bar) renderProgress() (string, int) {
   433  	var result, curText, totText, label string
   434  	var size int
   435  
   436  	if b.settings.IsSize {
   437  		curText, totText, label = getPrettyCTSize(b.current, b.total)
   438  	} else {
   439  		curText, totText, label = getPrettyCTNum(b.current, b.total)
   440  	}
   441  
   442  	size = (len(totText) * 2) + len(label) + 1
   443  
   444  	if label == "" {
   445  		result = fmt.Sprintf("%"+strconv.Itoa(size)+"s", curText+"/"+totText)
   446  	} else {
   447  		result = fmt.Sprintf("%"+strconv.Itoa(size)+"s", curText+"/"+totText+label)
   448  	}
   449  
   450  	if fmtc.DisableColors || b.settings.ProgressColorTag == "" {
   451  		return result, size
   452  	}
   453  
   454  	return b.settings.ProgressColorTag + result + "{!}", size
   455  }
   456  
   457  // renderSpeed returns speed text
   458  func (b *Bar) renderSpeed(speed float64) (string, int) {
   459  	var result string
   460  
   461  	if b.settings.IsSize {
   462  		result = fmt.Sprintf("%9s/s", fmtutil.PrettySize(speed, " "))
   463  	} else {
   464  		result = formatSpeedNum(speed)
   465  	}
   466  
   467  	if fmtc.DisableColors || b.settings.SpeedColorTag == "" {
   468  		return result, len(result)
   469  	}
   470  
   471  	return b.settings.SpeedColorTag + result + "{!}", len(result)
   472  }
   473  
   474  // renderRemaining returns remaining text
   475  func (b *Bar) renderRemaining(remaining time.Duration) (string, int) {
   476  	var result string
   477  	var min, sec int
   478  
   479  	d := int(remaining.Seconds())
   480  
   481  	if d >= 60 {
   482  		min = d / 60
   483  		sec = d % 60
   484  	} else {
   485  		sec = d
   486  	}
   487  
   488  	result = fmt.Sprintf("%2d:%02d", min, sec)
   489  
   490  	if fmtc.DisableColors || b.settings.RemainingColorTag == "" {
   491  		return result, len(result)
   492  	}
   493  
   494  	return b.settings.RemainingColorTag + result + "{!}", len(result)
   495  }
   496  
   497  // renderBar returns bar graphics
   498  func (b *Bar) renderBar(dataSize int) string {
   499  	size := mathutil.Max(5, mathutil.Max(MIN_WIDTH, b.settings.Width)-dataSize)
   500  
   501  	if b.total <= 0 {
   502  		return b.renderPlaceholder(size)
   503  	}
   504  
   505  	if b.current >= b.total {
   506  		switch fmtc.DisableColors || b.settings.BarFgColorTag == "" {
   507  		case true:
   508  			return strings.Repeat(PROGRESS_BAR_SYMBOL, size)
   509  		case false:
   510  			return b.settings.BarFgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, size) + "{!}"
   511  		}
   512  	}
   513  
   514  	cur := int((float64(b.current) / float64(b.total)) * float64(size))
   515  
   516  	if fmtc.DisableColors || b.settings.BarFgColorTag == "" {
   517  		return strings.Repeat(PROGRESS_BAR_SYMBOL, cur) + strings.Repeat(" ", size-cur)
   518  	}
   519  
   520  	return b.settings.BarFgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, cur) + b.settings.BarBgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, size-cur) + "{!}"
   521  }
   522  
   523  // renderPlaceholder returns placeholder bar graphics
   524  func (b *Bar) renderPlaceholder(size int) string {
   525  	var result string
   526  
   527  	disableColors := fmtc.DisableColors || b.settings.BarFgColorTag == ""
   528  
   529  	for i := 0; i < size; i++ {
   530  		if disableColors {
   531  			if i%3 == b.phCounter {
   532  				result += PROGRESS_BAR_SYMBOL
   533  			} else {
   534  				result += " "
   535  			}
   536  		} else {
   537  			if i%3 == b.phCounter {
   538  				result += b.settings.BarFgColorTag
   539  			} else {
   540  				result += b.settings.BarBgColorTag
   541  			}
   542  
   543  			result += PROGRESS_BAR_SYMBOL
   544  		}
   545  	}
   546  
   547  	b.phCounter++
   548  
   549  	if b.phCounter == 3 {
   550  		b.phCounter = 0
   551  	}
   552  
   553  	if disableColors {
   554  		return result
   555  	}
   556  
   557  	return result + "{!}"
   558  }
   559  
   560  // ////////////////////////////////////////////////////////////////////////////////// //
   561  
   562  // Read reads data and updates progress bar
   563  func (r *passThruReader) Read(p []byte) (int, error) {
   564  	n, err := r.Reader.Read(p)
   565  
   566  	if n > 0 {
   567  		r.bar.Add(n)
   568  	}
   569  
   570  	return n, err
   571  }
   572  
   573  // Write writes data and updates progress bar
   574  func (w *passThruWriter) Write(p []byte) (int, error) {
   575  	n, err := w.Writer.Write(p)
   576  
   577  	if n > 0 {
   578  		w.bar.Add(n)
   579  	}
   580  
   581  	return n, err
   582  }
   583  
   584  // ////////////////////////////////////////////////////////////////////////////////// //
   585  
   586  // getPrettyCTSize returns formatted current/total size text
   587  func getPrettyCTSize(current, total int64) (string, string, string) {
   588  	var mod float64
   589  	var label string
   590  
   591  	switch {
   592  	case total > 1024*1024*1024:
   593  		mod = 1024 * 1024 * 1024
   594  		label = " GB"
   595  	case total > 1024*1024:
   596  		mod = 1024 * 1024
   597  		label = " MB"
   598  	case total > 1024:
   599  		mod = 1024
   600  		label = " KB"
   601  	default:
   602  		mod = 1
   603  		label = " B"
   604  	}
   605  
   606  	curText := fmt.Sprintf("%.1f", float64(current)/mod)
   607  	totText := fmt.Sprintf("%.1f", float64(total)/mod)
   608  
   609  	return curText, totText, label
   610  }
   611  
   612  // getPrettyCTNum returns formatted current/total number text
   613  func getPrettyCTNum(current, total int64) (string, string, string) {
   614  	var mod float64
   615  	var label, curText, totText string
   616  
   617  	switch {
   618  	case total > 1000*1000*1000:
   619  		mod = 1000 * 1000 * 1000
   620  		label = "B"
   621  	case total > 1000*1000:
   622  		mod = 1000 * 1000
   623  		label = "M"
   624  	case total > 1000:
   625  		mod = 1000
   626  		label = "K"
   627  	default:
   628  		mod = 1
   629  	}
   630  
   631  	if total > 1000 {
   632  		curText = fmt.Sprintf("%.1f", float64(current)/mod)
   633  		totText = fmt.Sprintf("%.1f", float64(total)/mod)
   634  	} else {
   635  		curText = fmt.Sprintf("%d", int(float64(current)/mod))
   636  		totText = fmt.Sprintf("%d", int(float64(total)/mod))
   637  	}
   638  
   639  	return curText, totText, label
   640  }
   641  
   642  // formatSpeedNum formats speed number
   643  func formatSpeedNum(s float64) string {
   644  	var mod float64
   645  	var label string
   646  
   647  	switch {
   648  	case s > 1000.0*1000.0*1000.0:
   649  		mod = 1000.0 * 1000.0 * 1000.0
   650  		label = "B"
   651  	case s > 1000.0*1000.0:
   652  		mod = 1000.0 * 1000.0
   653  		label = "M"
   654  	case s > 1000.0:
   655  		mod = 1000.0
   656  		label = "K"
   657  	default:
   658  		mod = 1
   659  	}
   660  
   661  	return fmt.Sprintf("%6g%s/s", fmtutil.Float(s/mod), label)
   662  }