Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(progress): the progress cmd for #153 #462

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,50 @@ gum pager < README.md

<img src="https://stuff.charm.sh/gum/pager.gif" width="600" alt="Shell running gum pager" />

#### Progress

Show progress with just plain text or a progress bar given a limit. <br>

Show progress of something which has a determined length.
```bash
# this example is only meant for illustration purpose.
readarray files < <(find "$HOME" -type f 2>/dev/null)

<<<"${files[@]}" $gum progress -o --limit ${#files[@]} --title 'checking...' | while read -r file; do
md5sum "$file" >> /tmp/cksums.txt 2>/dev/null
done
```

Given no limit progress is printed as text only.
```bash
find / -type d 2> /dev/null | gum progress --show-output > /tmp/dump.txt
```

Use a custom format.
```bash
find / -type d 2> /dev/null | gum progress -f '{Iter} ~ {Elapsed}' -o > /tmp/dump.txt
```

Using a different progress indicator

```bash
{
sleep 2s
echo ":step:Long process 1 completed"

sleep 2s
echo ":step:Long process 2 completed"

sleep 2s
echo ":step:Long process 3 completed"
} | gum progress -l 3 --progress-indicator ':step:' -o --hide-progress-indicator
```

**Note:** when using a --progress-indicator != '\n' (the default) with output
going to the terminal (using -o/--output not piped)
the lines will still be printed line wise. This has no influence on the
measurement of progress!

#### Spin

Display a spinner while running a script or command. The spinner will
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw=
github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q=
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 v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
Expand Down
7 changes: 7 additions & 0 deletions gum.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/charmbracelet/gum/log"
"github.com/charmbracelet/gum/man"
"github.com/charmbracelet/gum/pager"
"github.com/charmbracelet/gum/progress"
"github.com/charmbracelet/gum/spin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/gum/table"
Expand Down Expand Up @@ -137,6 +138,12 @@ type Gum struct {
//
Pager pager.Options `cmd:"" help:"Scroll through a file"`

// Progress provides a shell script interface for the progress bubble.
// https://github.com/charmbracelet/bubbles/tree/master/progress
//
// On top ... when no limit is set some other progress information is displayed.
Progress progress.Options `cmd:"" help:"Show progressbar"`

// Spin provides a shell script interface for the spinner bubble.
// https://github.com/charmbracelet/bubbles/tree/master/spinner
//
Expand Down
64 changes: 64 additions & 0 deletions progress/barinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package progress

import (
"math"
"time"
)

type barInfo struct {
iter uint
title string
limit uint
incrementTs []time.Time
}

func newBarInfo(title string, limit uint) *barInfo {
info := &barInfo{
title: title,
limit: limit,
incrementTs: make([]time.Time, 0, limit),
}
info.incrementTs = append(info.incrementTs, time.Now())
return info
}

func (self *barInfo) Update(progressAmount uint) {
self.iter += progressAmount

now := time.Now()
for i := uint(0); i < progressAmount; i++ {
self.incrementTs = append(self.incrementTs, now)
}
}

func (self *barInfo) Elapsed() time.Duration {
return time.Now().Sub(self.incrementTs[0]).Truncate(time.Second)
}

func (self *barInfo) Pct() int {
pct := math.Round(safeDivide(float64(self.iter), float64(self.limit)) * 100)
return int(pct)
}

func (self *barInfo) Avg() time.Duration {
if len(self.incrementTs) < 2 {
return time.Now().Sub(self.incrementTs[0])
}
var sum time.Duration
for i := 1; i < len(self.incrementTs); i++ {
sum += self.incrementTs[i].Sub(self.incrementTs[i-1])
}
return (sum / time.Duration(self.iter))
}

func (self *barInfo) Eta() time.Duration {
if self.iter >= self.limit {
return 0
}
avg := self.Avg()
if avg == 0 {
return 0
}

return time.Duration(self.limit-self.iter) * avg
}
71 changes: 71 additions & 0 deletions progress/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Package progress provides a simple progress indicator
// for tracking the progress for input provided via stdin.
//
// It shows a progress bar when the limit is known and some simple stats when not.
//
// ------------------------------------
// #!/bin/bash
//
// urls=(
//
// "http://example.com/file1.txt"
// "http://example.com/file2.txt"
// "http://example.com/file3.txt"
//
// )
//
// for url in "${urls[@]}"; do
//
// wget -q -nc "$url"
// echo "Downloaded: $url"
//
// done | gum progress --show-output --limit ${#urls[@]}
// ------------------------------------
package progress

import (
"bufio"
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-isatty"
)

func (o Options) GetFormatString() string {
if o.Format != "" {
return o.Format
}

switch {
case o.Limit == 0 && o.Title == "":
return "[Elapsed ~ {Elapsed}] Iter {Iter}"
case o.Limit == 0 && o.Title != "":
return "[Elapsed ~ {Elapsed}] Iter {Iter} ~ {Title}"
case o.Limit > 0 && o.Title == "":
return "{Bar} {Pct}"
case o.Limit > 0 && o.Title != "":
return "{Title} ~ {Bar} {Pct}"
default:
return "{Iter}"
}
}

func (o Options) Run() error {
m := &model{
reader: bufio.NewReader(os.Stdin),
output: o.ShowOutput,
isTTY: isatty.IsTerminal(os.Stdout.Fd()),
progressIndicator: o.ProgressIndicator,
hideProgressIndicator: o.HideProgressIndicator,

bfmt: newBarFormatter(o.GetFormatString(), o.ProgressColor),
binfo: newBarInfo(o.TitleStyle.ToLipgloss().Render(o.Title), o.Limit),
}
p := tea.NewProgram(m, tea.WithOutput(os.Stderr))
if _, err := p.Run(); err != nil {
return fmt.Errorf("failed to run progress: %w", err)
}

return m.err
}
107 changes: 107 additions & 0 deletions progress/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package progress

import (
"fmt"
"regexp"
"strings"
"time"

"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/lipgloss"
)

type barFormatter struct {
pbar progress.Model
numBars int
tplstr string
}

var barPlaceholderRe = regexp.MustCompile(`{\s*Bar\s*}`)
var nonBarPlaceholderRe = regexp.MustCompile(`{\s*(Title|Elapsed|Iter|Avg|Pct|Eta|Remaining|Limit)\s*}`)

func newBarFormatter(tplstr string, barColor string) *barFormatter {
var bar progress.Model
if barColor != "" {
bar = progress.New(progress.WithoutPercentage(), progress.WithSolidFill(barColor))
} else {
bar = progress.New(progress.WithoutPercentage())
}
barfmt := &barFormatter{
pbar: bar,
tplstr: tplstr,
numBars: len(barPlaceholderRe.FindAllString(tplstr, -1)),
}
return barfmt
}

func (self *barFormatter) Render(info *barInfo, maxWidth int) string {
rendered := nonBarPlaceholderRe.ReplaceAllStringFunc(self.tplstr, func(s string) string {
switch strings.TrimSpace(s[1 : len(s)-1]) {
case "Title":
return info.title
case "Iter":
return fmt.Sprint(info.iter)
case "Limit":
if info.limit == 0 {
return s
}
return fmt.Sprint(info.limit)
case "Elapsed":
return info.Elapsed().String()
case "Pct":
if info.limit == 0 {
return s
}
return fmt.Sprintf("%d%%", info.Pct())
case "Avg":
return info.Avg().Round(time.Second).String()
case "Remaining":
if info.limit == 0 {
return s
}
return info.Eta().Round(time.Second).String()
case "Eta":
if info.limit == 0 {
return s
}
return time.Now().Add(info.Eta()).Format(time.TimeOnly)
default:
return ""
}
})

if info.limit > 0 && self.numBars > 0 {
self.pbar.Width = max(0, (maxWidth-lipgloss.Width(rendered))/int(self.numBars))
bar := self.pbar.ViewAs(safeDivide(float64(info.iter), float64(info.limit)))
rendered = barPlaceholderRe.ReplaceAllLiteralString(rendered, bar)
}
return rendered
}

func min(a, b uint) uint {
if a < b {
return a
}
return b
}

func minI(a, b int) int {
if a < b {
return a
}
return b
}

func max(a, b int) int {
if a > b {
return a
}
return b
}

func safeDivide(a, b float64) float64 {
if b == 0 {
return 0
}
return a / b
}
18 changes: 18 additions & 0 deletions progress/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package progress

import (
"github.com/charmbracelet/gum/style"
)

type Options struct {
Title string `help:"Text to display to user while spinning" env:"GUM_PROGRESS_TITLE"`
TitleStyle style.Styles `embed:"" prefix:"title." envprefix:"GUM_PROGRESS_TITLE_"`
Format string `short:"f" help:"What format to use for rendering the bar. Choose from: {Iter}, {Elapsed}, {Title} and {Avg} or see --limit for more options. Unknown options remain untouched." envprefix:"GUM_PROGRESS_FORMAT"`
ProgressColor string `help:"Set the color for the progress" envprefix:"GUM_PROGRESS_PROGRESS_COLOR"`

ProgressIndicator string `help:"What indicator to use for counting progress" default:"\n" env:"GUM_PROGRESS_PROGRESS_INDICATOR"`
HideProgressIndicator bool `help:"Don't show the --progress-indicator in the output. Only makes sense in combination with --show-output" default:"false"`
ShowOutput bool `short:"o" help:"Print what gum reads" default:"false"`

Limit uint `short:"l" help:"Species how many items there are (enables {Bar}, {Limit}, {Remaining}, {Eta} and {Pct} to be used in --format)"`
}
Loading