fix: add download cancel via context and update progressbar
This commit is contained in:
246
internal/dl/progress_tui.go
Normal file
246
internal/dl/progress_tui.go
Normal file
@ -0,0 +1,246 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user