From 3f578882a651f74d4e701d8682e6c76752da76b5 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 5 Apr 2025 13:45:16 -0700 Subject: Refactor into reusable libraries --- cmd/progress-bar/progress-bar.go | 53 ++++++++++++++++++++++++++++ go.mod | 7 ++-- go.sum | 2 ++ progress-bar.go | 76 ---------------------------------------- progress.go | 63 +++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 78 deletions(-) create mode 100644 cmd/progress-bar/progress-bar.go delete mode 100644 progress-bar.go create mode 100644 progress.go diff --git a/cmd/progress-bar/progress-bar.go b/cmd/progress-bar/progress-bar.go new file mode 100644 index 0000000..65079ef --- /dev/null +++ b/cmd/progress-bar/progress-bar.go @@ -0,0 +1,53 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strconv" + "time" + + "gitlab.com/rhogenson/progress-bar/progress" +) + +var n = flag.Duration("n", 2*time.Minute, "polling interval") + +func run() error { + args := flag.Args() + if len(args) < 2 { + return errors.New("usage error") + } + maxStr := args[0] + maxVal, err := strconv.Atoi(maxStr) + if err != nil { + return fmt.Errorf("max: %s", err) + } + cmd := args[1:] + bar := progress.New(maxVal) + for i := 0; ; i++ { + outputBytes, err := exec.Command(cmd[0], cmd[1:]...).Output() + if err != nil { + fmt.Println(err) + continue + } + output, err := strconv.Atoi(string(bytes.TrimSuffix(outputBytes, []byte("\n")))) + if err != nil { + fmt.Println(err) + continue + } + bar.Set(output) + bar.Print() + time.Sleep(*n) + } +} + +func main() { + flag.Parse() + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index 8d19cf8..9531fa9 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,10 @@ -module gitlab.com/rhogenson/progress-bar +module gitlab.com/rhogenson/progress-bar/progress go 1.24.1 -require golang.org/x/term v0.28.0 +require ( + gitlab.com/rhogenson/vecdeque v0.0.0-20250405201931-eb7fb2bc95c6 + golang.org/x/term v0.28.0 +) require golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index f05f63b..4af7e2b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +gitlab.com/rhogenson/vecdeque v0.0.0-20250405201931-eb7fb2bc95c6 h1:S1vSZ4W5v+eL/ayT5M4RDqPUHlAN2SKl/x65O9D4jPo= +gitlab.com/rhogenson/vecdeque v0.0.0-20250405201931-eb7fb2bc95c6/go.mod h1:i++skCmDHJAQbyC5LqvJbehbEGCiO8EkrsPon6h0Jgo= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= diff --git a/progress-bar.go b/progress-bar.go deleted file mode 100644 index 0b1cefe..0000000 --- a/progress-bar.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "time" - - "golang.org/x/term" -) - -var n = flag.Duration("n", 2*time.Minute, "polling interval") - -func run() error { - cols, _, err := term.GetSize(int(os.Stdout.Fd())) - if err != nil { - return err - } - args := flag.Args() - if len(args) < 2 { - return errors.New("usage error") - } - maxStr := args[0] - maxVal, err := strconv.Atoi(maxStr) - if err != nil { - return fmt.Errorf("max: %s", err) - } - cmd := args[1:] - type measurement struct { - t time.Time - i int - } - var measurements []measurement - for i := 0; ; i++ { - outputBytes, err := exec.Command(cmd[0], cmd[1:]...).Output() - if err != nil { - fmt.Println(err) - continue - } - output, err := strconv.Atoi(string(bytes.TrimSuffix(outputBytes, []byte("\n")))) - if err != nil { - fmt.Println(err) - continue - } - p := output * cols / maxVal - if i > 0 { - fmt.Print("\033[2F\033[J") - } - fmt.Printf("%s>\n", strings.Repeat("=", max(p-1, 0))) - measurements = append(measurements, measurement{time.Now(), output}) - if len(measurements) > 20 { - measurements = measurements[1:] - } - deltaT := measurements[len(measurements)-1].t.Sub(measurements[0].t) - delta := measurements[len(measurements)-1].i - measurements[0].i - if delta == 0 { - fmt.Println() - } else { - fmt.Printf("ETA: %s\n", time.Duration(float64(maxVal-output)/float64(delta)*float64(deltaT)).Round(time.Second)) - } - time.Sleep(*n) - } -} - -func main() { - flag.Parse() - if err := run(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/progress.go b/progress.go new file mode 100644 index 0000000..7660605 --- /dev/null +++ b/progress.go @@ -0,0 +1,63 @@ +package progress + +import ( + "fmt" + "os" + "strings" + "time" + + "gitlab.com/rhogenson/vecdeque" + "golang.org/x/term" +) + +const measurements = 20 + +type measurement struct { + t time.Time + i int +} + +type Bar struct { + max int + measurements vecdeque.DQ[measurement] + cols int +} + +func New(max int) *Bar { + b := &Bar{max: max} + b.measurements.Grow(measurements) + return b +} + +func (b *Bar) Set(i int) { + if b.measurements.Len() == measurements { + b.measurements.PopFront() + } + b.measurements.PushBack(measurement{time.Now(), i}) +} + +func (b *Bar) Print() { + first := b.measurements.Get(0) + last := b.measurements.Get(b.measurements.Len() - 1) + deltaT := last.t.Sub(first.t) + delta := last.i - first.i + eta := time.Duration(-1) + if delta != 0 { + eta = time.Duration(float64(b.max-last.i) / float64(delta) * float64(deltaT)) + } + + if b.cols == 0 { + var err error + if b.cols, _, err = term.GetSize(int(os.Stderr.Fd())); err != nil { + fmt.Fprintf(os.Stderr, "Warning: unable to determine terminal size: %s\n", err) + } + } else { + p := last.i * b.cols / b.max + fmt.Fprintf(os.Stderr, "\033[2F\033[J%s>\n", strings.Repeat("=", max(p-1, 0))) + } + if eta < 0 { + fmt.Fprintln(os.Stderr, "ETA: calculating...") + } else { + fmt.Fprintf(os.Stderr, "ETA: %s\n", eta.Round(time.Second)) + } +} -- cgit v1.3.1