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 { 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() // Don't add TUI if the header doesn't include content size // it's impossible see progress without total if resp.ContentLength <= 0 { fmt.Println("can't parse content length, aborting download") os.Exit(1) } 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) { p.Send(progressMsg(ratio)) }, } m := model{ pw: pw, progress: progress.New(progress.WithDefaultGradient()), } // Start Bubble Tea p = tea.NewProgram(m) // Start the download go pw.Start() if _, err := p.Run(); err != nil { fmt.Println("error running program:", err) os.Exit(1) } }