From 5dfbef65c193165b89d63310e964e4aa3d01caaf Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 5 May 2022 21:50:49 -0700 Subject: [PATCH] docs: add another progress bar example --- examples/progress-download/README.md | 11 +++ examples/progress-download/main.go | 108 +++++++++++++++++++++++++++ examples/progress-download/tui.go | 86 +++++++++++++++++++++ 3 files changed, 205 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..7d95c548f8 --- /dev/null +++ b/examples/progress-download/README.md @@ -0,0 +1,11 @@ +# Download Progress +This example was built based on [this](https://github.com/charmbracelet/bubbles/discussions/127) discussion. +This example demonstrates how to download a file from a given URL, and show its progress with a [progress Bubble](https://github.com/charmbracelet/bubbles/). +The status of the download is updated with [`io.TeeReader`](https://pkg.go.dev/io#TeeReader). +This calls `Write` which is where we send the updated status with `Program#Send()` + +## How to Run +`go build .` in this directory on your machine (in examples/download-progress) +then run `./download-progress --url="https://download.blender.org/demo/color_vortex.blend"` this can be whatever file you'd like to download. +Note: the current version will not show a TUI for downloads that do not provide the ContentLength header field. + 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") +}