// ALR - Any Linux Repository // Copyright (C) 2025 Евгений Храмов // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package dl import ( "fmt" "io" "math" "os" "time" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/leonelquinteros/gotext" ) type model struct { progress progress.Model spinner spinner.Model percent float64 speed float64 done bool useSpinner bool filename string total int64 downloaded int64 elapsed time.Duration remaining time.Duration width int } func (m model) Init() tea.Cmd { if m.useSpinner { return m.spinner.Tick } return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.done { return m, tea.Quit } switch msg := msg.(type) { case progressUpdate: m.percent = msg.percent m.speed = msg.speed m.downloaded = msg.downloaded m.total = msg.total m.elapsed = time.Duration(msg.elapsed) * time.Second m.remaining = time.Duration(msg.remaining) * time.Second if m.percent >= 1.0 { m.done = true } return m, nil case tea.WindowSizeMsg: m.width = msg.Width return m, nil case progress.FrameMsg: if !m.useSpinner { progressModel, cmd := m.progress.Update(msg) m.progress = progressModel.(progress.Model) return m, cmd } case spinner.TickMsg: if m.useSpinner { spinnerModel, cmd := m.spinner.Update(msg) m.spinner = spinnerModel return m, cmd } case tea.KeyMsg: if msg.String() == "q" { return m, tea.Quit } } return m, nil } func (m model) View() string { if m.done { return gotext.Get("%s: done!\n", m.filename) } if m.useSpinner { return gotext.Get( "%s %s downloading at %s/s\n", m.filename, m.spinner.View(), prettyByteSize(int64(m.speed)), ) } leftPart := m.filename rightPart := fmt.Sprintf("%.2f%% (%s/%s, %s/s) [%v:%v]\n", m.percent*100, prettyByteSize(m.downloaded), prettyByteSize(m.total), prettyByteSize(int64(m.speed)), m.elapsed, m.remaining, ) m.progress.Width = m.width - len(leftPart) - len(rightPart) - 6 bar := m.progress.ViewAs(m.percent) return fmt.Sprintf( "%s %s %s", leftPart, bar, rightPart, ) } func prettyByteSize(b int64) string { bf := float64(b) for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { if math.Abs(bf) < 1024.0 { return fmt.Sprintf("%3.1f%sB", bf, unit) } bf /= 1024.0 } return fmt.Sprintf("%.1fYiB", bf) } type progressUpdate struct { percent float64 speed float64 total int64 downloaded int64 elapsed float64 remaining float64 } type ProgressWriter struct { baseWriter io.WriteCloser total int64 downloaded int64 startTime time.Time onProgress func(progressUpdate) lastReported time.Time doneChan chan struct{} } func (pw *ProgressWriter) Write(p []byte) (int, error) { n, err := pw.baseWriter.Write(p) if err != nil { return n, err } pw.downloaded += int64(n) now := time.Now() elapsed := now.Sub(pw.startTime).Seconds() speed := float64(pw.downloaded) / elapsed var remaining, percent float64 if pw.total > 0 { remaining = (float64(pw.total) - float64(pw.downloaded)) / speed percent = float64(pw.downloaded) / float64(pw.total) } if now.Sub(pw.lastReported) > 100*time.Millisecond { pw.onProgress(progressUpdate{ percent: percent, speed: speed, total: pw.total, downloaded: pw.downloaded, elapsed: elapsed, remaining: remaining, }) pw.lastReported = now } return n, nil } func (pw *ProgressWriter) Close() error { pw.onProgress(progressUpdate{ percent: 1, speed: 0, downloaded: pw.downloaded, }) <-pw.doneChan return nil } func NewProgressWriter(base io.WriteCloser, max int64, filename string, out io.Writer) *ProgressWriter { var m *model if max == -1 { m = &model{ spinner: spinner.New(), useSpinner: true, filename: filename, } m.spinner.Spinner = spinner.Dot } else { m = &model{ progress: progress.New( progress.WithDefaultGradient(), progress.WithoutPercentage(), ), useSpinner: false, filename: filename, } } p := tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(out), ) pw := &ProgressWriter{ baseWriter: base, total: max, startTime: time.Now(), doneChan: make(chan struct{}), onProgress: func(update progressUpdate) { p.Send(update) }, } go func() { defer close(pw.doneChan) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error running progress writer: %v\n", err) os.Exit(1) } }() return pw }