summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/progress-bar/progress-bar.go53
-rw-r--r--go.mod23
-rw-r--r--go.sum41
-rw-r--r--progress-bar.go129
-rw-r--r--progress.go90
5 files changed, 189 insertions, 147 deletions
diff --git a/cmd/progress-bar/progress-bar.go b/cmd/progress-bar/progress-bar.go
deleted file mode 100644
index 19050c8..0000000
--- a/cmd/progress-bar/progress-bar.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package main
-
-import (
- "bytes"
- "errors"
- "flag"
- "fmt"
- "os"
- "os/exec"
- "strconv"
- "time"
-
- "gitlab.com/rhogenson/progress-bar"
-)
-
-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 d6390d4..8d52c58 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,27 @@ module gitlab.com/rhogenson/progress-bar
go 1.24.1
require (
+ github.com/charmbracelet/bubbles v0.20.0
+ github.com/charmbracelet/bubbletea v1.3.4
gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030
- golang.org/x/term v0.31.0
)
-require golang.org/x/sys v0.32.0 // indirect
+require (
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/harmonica v0.2.0 // indirect
+ github.com/charmbracelet/lipgloss v1.0.0 // indirect
+ github.com/charmbracelet/x/ansi v0.8.0 // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ golang.org/x/sync v0.12.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 639cbcb..8226c8b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,43 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
+github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
+github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
+github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
+github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
+github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030 h1:ElfzUwCcfm4HHxJkMfoEn5w8VOoI+XRYfa9Vdo7Plno=
gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030/go.mod h1:+JcaVhyJ7VgoRMo4PsMgOCDoVnIOLts4CkDEAYkU4o4=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
-golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
diff --git a/progress-bar.go b/progress-bar.go
new file mode 100644
index 0000000..82e7cb0
--- /dev/null
+++ b/progress-bar.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/bubbles/progress"
+ tea "github.com/charmbracelet/bubbletea"
+ "gitlab.com/rhogenson/deque"
+)
+
+const (
+ padding = 2
+ maxWidth = 80
+)
+
+type measurement struct {
+ t time.Time
+ i int
+}
+
+var (
+ maxVal int
+ bar progress.Model
+ measurements deque.Deque[measurement]
+ eta time.Duration = -1
+)
+
+type (
+ measurementMsg int
+ quitMsg struct{}
+)
+
+type model struct{}
+
+func (model) Init() tea.Cmd {
+ return nil
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ bar.Width = msg.Width - padding*2
+ return m, nil
+
+ case measurementMsg:
+ n := int(msg)
+ if measurements.Len() > 0 && measurements.At(measurements.Len()-1).i > n {
+ measurements.Reset()
+ eta = -1
+ }
+ now := time.Now()
+ for measurements.Len() > 2 && now.Sub(measurements.At(0).t) > 2*time.Minute {
+ measurements.PopFront()
+ }
+ measurements.PushBack(measurement{now, n})
+ first := measurements.At(0)
+ deltaT := now.Sub(first.t)
+ delta := n - first.i
+ if delta != 0 {
+ eta = time.Duration(float64(maxVal-n) / float64(delta) * float64(deltaT))
+ }
+ return m, bar.SetPercent(float64(n) / float64(maxVal))
+
+ case quitMsg:
+ return m, tea.Quit
+
+ // FrameMsg is sent when the progress bar wants to animate itself
+ case progress.FrameMsg:
+ progressModel, cmd := bar.Update(msg)
+ bar = progressModel.(progress.Model)
+ return m, cmd
+
+ default:
+ return m, nil
+ }
+}
+
+func (m model) View() string {
+ etaStr := "calculating..."
+ if eta >= 0 {
+ etaStr = eta.Round(time.Second).String()
+ }
+ pad := strings.Repeat(" ", padding)
+ return "\n" +
+ pad + bar.View() + "\n" +
+ pad + "ETA: " + etaStr + "\n"
+}
+
+func run() error {
+ flag.Parse()
+ args := flag.Args()
+ if len(args) != 1 {
+ return errors.New("usage error")
+ }
+ var err error
+ maxVal, err = strconv.Atoi(args[0])
+ if err != nil {
+ return err
+ }
+ bar = progress.New(progress.WithDefaultGradient())
+ program := tea.NewProgram(model{}, tea.WithInput(nil))
+ go func() {
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ n, err := strconv.Atoi(scanner.Text())
+ if err != nil {
+ continue
+ }
+ program.Send(measurementMsg(n))
+ }
+ program.Send(quitMsg{})
+ }()
+ _, err = program.Run()
+ return err
+}
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/progress.go b/progress.go
deleted file mode 100644
index 3a638ed..0000000
--- a/progress.go
+++ /dev/null
@@ -1,90 +0,0 @@
-// Package progress implements a simple command line progress bar.
-//
-// Example:
-//
-// import (
-// time"
-//
-// "gitlab.com/rhogenson/progress-bar"
-// )
-//
-// func main() {
-// b := progress.New(100)
-// for i := range 100 {
-// b.Set(i)
-// b.Print()
-// time.Sleep(time.Second)
-// }
-// }
-package progress
-
-import (
- "fmt"
- "os"
- "strings"
- "time"
-
- "gitlab.com/rhogenson/deque"
- "golang.org/x/term"
-)
-
-type measurement struct {
- t time.Time
- i int
-}
-
-// Bar is a progess bar.
-type Bar struct {
- max int
- measurements deque.Deque[measurement]
- secondPrint bool
-}
-
-func New(max int) *Bar {
- return &Bar{max: max}
-}
-
-// Set sets the current value to val.
-func (b *Bar) Set(val int) {
- // Measurements must be strictly increasing
- if b.measurements.Len() > 0 && b.measurements.At(b.measurements.Len()-1).i > val {
- b.measurements.Reset()
- }
- b.measurements.PushBack(measurement{time.Now(), val})
-}
-
-// Print shows the progress bar on standard error.
-func (b *Bar) Print() {
- if b.measurements.Len() == 0 {
- return
- }
-
- now := time.Now()
- for b.measurements.Len() > 2 && now.Sub(b.measurements.At(0).t) > 2*time.Minute {
- b.measurements.PopFront()
- }
- first := b.measurements.At(0)
- last := b.measurements.At(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.secondPrint {
- fmt.Fprint(os.Stderr, "\033[2F\033[J")
- }
- b.secondPrint = true
- if cols, _, err := term.GetSize(int(os.Stderr.Fd())); err != nil {
- fmt.Fprintf(os.Stderr, "Unable to determine terminal size: %s\n", err)
- } else {
- p := last.i * cols / b.max
- fmt.Fprintf(os.Stderr, "%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))
- }
-}