247 lines
5.1 KiB
Go
247 lines
5.1 KiB
Go
|
// 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
|
||
|
}
|