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

docs: add another progress bar example #270

Merged
merged 2 commits into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/progress-download/README.md
@@ -0,0 +1,34 @@
# Download Progress

This example demonstrates how to download a file from a URL and show its
progress with a [Progress Bubble][progress].

In this case we're getting download progress with an [`io.TeeReader`][tee] and
sending progress `Msg`s to the `Program` with `Program.Send()`.

## How to Run

Build the application with `go build .`, then run with a `--url` argument
specifying the URL of the file to download. For example:

```
./download-progress --url="https://download.blender.org/demo/color_vortex.blend"
```

Note that in this example a TUI will not be shown for URLs that do not respond
with a ContentLength header.

* * *

This example originally came from [this discussion][discussion].

* * *

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Charm热爱开源 • Charm loves open source


[progress]: https://github.com/charmbracelet/bubbles/
[tee]: https://pkg.go.dev/io#TeeReader
[discussion]: https://github.com/charmbracelet/bubbles/discussions/127
108 changes: 108 additions & 0 deletions examples/progress-download/main.go
@@ -0,0 +1,108 @@
package main

import (
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"

"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
)

var p *tea.Program

type progressWriter struct {
total int
downloaded int
file *os.File
reader io.Reader
onProgress func(float64)
}

func (pw *progressWriter) Start() {
// TeeReader calls pw.Write() each time a new response is received
_, err := io.Copy(pw.file, io.TeeReader(pw.reader, pw))
if err != nil {
if p != nil {
p.Send(progressErrMsg{err})
}
}
}

func (pw *progressWriter) Write(p []byte) (int, error) {
pw.downloaded += len(p)
if pw.total > 0 && pw.onProgress != nil {
pw.onProgress(float64(pw.downloaded) / float64(pw.total))
}
return len(p), nil
}

func getResponse(url string) (*http.Response, error) {
resp, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("receiving status of %d for url: %s", resp.StatusCode, url)
}
return resp, nil
}

func main() {
url := flag.String("url", "", "url for the file to download")
flag.Parse()

if *url == "" {
flag.Usage()
os.Exit(1)
}

resp, err := getResponse(*url)
if err != nil {
fmt.Println("could not get response", err)
os.Exit(1)
}
defer resp.Body.Close()

filename := filepath.Base(*url)
file, err := os.Create(filename)
if err != nil {
fmt.Println("could not create file: ", err)
os.Exit(1)
}
defer file.Close()

pw := &progressWriter{
total: int(resp.ContentLength),
file: file,
reader: resp.Body,
onProgress: func(ratio float64) {
if p != nil {
p.Send(progressMsg(ratio))
}
},
}

m := model{
pw: pw,
progress: progress.New(progress.WithDefaultGradient()),
}

// Start the download
go pw.Start()

// Don't add TUI if the header doesn't include content size
// it's impossible see progress without total
if resp.ContentLength > 0 {
// Start Bubble Tea
p = tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Println("error running program:", err)
os.Exit(1)
}
}
}
86 changes: 86 additions & 0 deletions examples/progress-download/tui.go
@@ -0,0 +1,86 @@
package main

import (
"strings"
"time"

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

var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render

const (
padding = 2
maxWidth = 80
)

type progressMsg float64

type progressErrMsg struct{ err error }

func finalPause() tea.Cmd {
return tea.Tick(time.Millisecond*750, func(_ time.Time) tea.Msg {
return nil
})
}

type model struct {
url, path string
pw *progressWriter
progress progress.Model
err error
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit

case tea.WindowSizeMsg:
m.progress.Width = msg.Width - padding*2 - 4
if m.progress.Width > maxWidth {
m.progress.Width = maxWidth
}
return m, nil

case progressErrMsg:
m.err = msg.err
return m, tea.Quit

case progressMsg:
var cmds []tea.Cmd

if msg >= 1.0 {
cmds = append(cmds, tea.Sequentially(finalPause(), tea.Quit))
}

cmds = append(cmds, m.progress.SetPercent(float64(msg)))
return m, tea.Batch(cmds...)

// FrameMsg is sent when the progress bar wants to animate itself
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd

default:
return m, nil
}
}

func (m model) View() string {
if m.err != nil {
return "Error downloading: " + m.err.Error() + "\n"
}

pad := strings.Repeat(" ", padding)
return "\n" +
pad + m.progress.View() + "\n\n" +
pad + helpStyle("Press any key to quit")
}