From a2d0ac9d38f384a99581d918777b84c11edb3836 Mon Sep 17 00:00:00 2001 From: bashbunni <15822994+bashbunni@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:55:47 -0700 Subject: [PATCH] docs: add another progress bar example (#270) * docs: add another progress bar example * chore: copy edits Co-authored-by: Christian Rocha --- examples/progress-download/README.md | 34 +++++++++ examples/progress-download/main.go | 108 +++++++++++++++++++++++++++ examples/progress-download/tui.go | 86 +++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 examples/progress-download/README.md create mode 100644 examples/progress-download/main.go create mode 100644 examples/progress-download/tui.go diff --git a/examples/progress-download/README.md b/examples/progress-download/README.md new file mode 100644 index 0000000000..cdb4535923 --- /dev/null +++ b/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]. + +* * * + +The Charm logo + +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 diff --git a/examples/progress-download/main.go b/examples/progress-download/main.go new file mode 100644 index 0000000000..601aaec11e --- /dev/null +++ b/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) + } + } +} diff --git a/examples/progress-download/tui.go b/examples/progress-download/tui.go new file mode 100644 index 0000000000..a9b5c7b139 --- /dev/null +++ b/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") +}