gopass/pkg/termio/progress.go
Dominik Schulz f58454452f
Increase test coverage (#2461)
RELEASE_NOTES=n/a

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2022-12-10 23:05:33 +01:00

245 lines
4.3 KiB
Go

package termio
import (
"fmt"
"math"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/dustin/go-humanize"
"github.com/fatih/color"
"golang.org/x/term"
)
const (
fps = 25
)
var now = time.Now
// ProgressBar is a gopass progress bar.
type ProgressBar struct {
// keep both int64 fields at the top to ensure correct
// 8-byte alignment on 32 bit systems. See https://golang.org/pkg/sync/atomic/#pkg-note-BUG
// and https://github.com/golang/go/issues/36606
total int64
current int64
mutex chan struct{}
lastUpd time.Time
Hidden bool
Bytes bool
}
// NewProgressBar creates a new progress bar.
func NewProgressBar(total int64) *ProgressBar {
return &ProgressBar{
total: total,
current: 0,
mutex: make(chan struct{}, 1),
}
}
// Add adds the given amount to the progress.
func (p *ProgressBar) Add(v int64) {
if p == nil {
return
}
cur := atomic.AddInt64(&p.current, v)
if max := atomic.LoadInt64(&p.total); cur > max {
atomic.StoreInt64(&p.total, cur)
}
p.print()
}
// Inc adds one to the progress.
func (p *ProgressBar) Inc() {
if p == nil {
return
}
cur := atomic.AddInt64(&p.current, 1)
if max := atomic.LoadInt64(&p.total); cur > max {
atomic.StoreInt64(&p.total, cur)
}
p.print()
}
// Set sets an arbitrary progress.
func (p *ProgressBar) Set(v int64) {
if p == nil {
return
}
atomic.StoreInt64(&p.current, v)
if max := atomic.LoadInt64(&p.total); v > max {
atomic.StoreInt64(&p.total, v)
}
p.print()
}
// Done finalizes the progress bar.
func (p *ProgressBar) Done() {
if p == nil {
return
}
if p.Hidden {
return
}
fmt.Fprintln(Stderr, "")
}
// Clear removes the progress bar.
func (p *ProgressBar) Clear() {
if p == nil {
return
}
clearLine()
}
// print will print the progress bar, if necessary.
func (p *ProgressBar) print() {
if p == nil {
return
}
if p.Hidden {
return
}
// try to lock
select {
case p.mutex <- struct{}{}:
// lock acquired
p.tryPrint()
<-p.mutex
default:
// lock not acquired
return
}
}
func (p *ProgressBar) tryPrint() {
ts := now()
if p.current == 0 || p.current >= p.total-1 || ts.Sub(p.lastUpd) > time.Second/fps {
p.lastUpd = ts
p.doPrint()
}
}
// doPrint redraws the current line.
// This method is based on https://github.com/muesli/goprogressbar/blob/master/progressbar.go#L96
func (p *ProgressBar) doPrint() {
clearLine()
cur, max, pct := p.percent()
pctStr := fmt.Sprintf("%.2f%%", pct*100)
// ensure consistent length
for len(pctStr) < 7 {
pctStr = " " + pctStr
}
termWidth, _, _ := term.GetSize(int(syscall.Stdin)) //nolint:unconvert
if termWidth < 0 {
// if we can determine the size (e.g. windows, fake term, mock)
// assume a sane default of 80
termWidth = 80
}
barWidth := uint(termWidth)
digits := int(math.Log10(float64(max))) + 1
// Log10(0) is undefined
if max < 1 {
digits = 1
}
text := fmt.Sprintf(fmt.Sprintf(" %%%dd / %%%dd ", digits, digits), cur, max)
if p.Bytes {
curStr := humanize.Bytes(uint64(cur))
maxStr := humanize.Bytes(uint64(max))
digits := len(maxStr) + 1
text = fmt.Sprintf(fmt.Sprintf(" %%%ds / %%%ds ", digits, digits), curStr, maxStr)
}
size := int(barWidth) - len(text) - len(pctStr) - 5
fill := int(math.Max(2, math.Floor((float64(size)*pct)+.5)))
fmt.Fprint(Stderr, text)
// not enough space
if size < 11 {
return
}
// Rgggggggggggmcyy
// Gooooooooooopass
tg := color.RedString("G")
to := strings.Repeat(color.GreenString("o"), gteZero(fill-5))
tp := strings.Repeat(color.YellowString("p"), boundedMin(1, fill-4))
ta := strings.Repeat(color.MagentaString("a"), boundedMin(1, fill-3))
ts := strings.Repeat(color.CyanString("s"), boundedMin(2, fill-1))
spc := strings.Repeat(" ", gteZero(size-fill))
fmt.Fprintf(Stderr, "[%s%s%s%s%s%s] %s ",
tg,
to,
tp,
ta,
ts,
spc,
pctStr,
)
}
func gteZero(a int) int {
if a >= 0 {
return a
}
return 0
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func boundedMin(a, b int) int {
return gteZero(min(a, b))
}
func (p *ProgressBar) percent() (int64, int64, float64) {
cur := atomic.LoadInt64(&p.current)
max := atomic.LoadInt64(&p.total)
pct := float64(cur) / float64(max)
if p.total < 1 {
if p.current < 1 {
pct = 1
} else {
pct = 0
}
}
// normalized between 0.0 and 1.0
return cur, max, math.Min(1, math.Max(0, pct))
}
func clearLine() {
fmt.Fprintf(Stderr, "\033[2K\r]")
}