Skip to content

Commit

Permalink
feat(progress): add progress cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
Sojamann committed Dec 22, 2023
1 parent d1145b4 commit 4cf16f4
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 0 deletions.
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.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
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

0 comments on commit 4cf16f4

Please sign in to comment.