diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2025-04-06 14:37:44 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2025-04-06 14:37:44 -0700 |
| commit | 2949987666d8b104ea305734fe5e159d97434240 (patch) | |
| tree | 9adab4dc76c390dec88ffd76b79d4155b12964bb | |
| parent | 6fc34c4c3cbb68f6f7cb8049a6d68bd315c07984 (diff) | |
| download | progress-bar-2949987666d8b104ea305734fe5e159d97434240.tar.zst | |
Bubbletea to make it pretty
| -rw-r--r-- | cmd/progress-bar/progress-bar.go | 53 | ||||
| -rw-r--r-- | go.mod | 23 | ||||
| -rw-r--r-- | go.sum | 41 | ||||
| -rw-r--r-- | progress-bar.go | 129 | ||||
| -rw-r--r-- | progress.go | 90 |
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) - } -} @@ -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 +) @@ -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)) - } -} |
