Files
dbbackup/internal/progress/progress.go
Alexander Renz dc6dfd8b2c
All checks were successful
CI/CD / Test (push) Successful in 1m16s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Successful in 3m13s
v3.42.31: Add schollz/progressbar for visual progress display
- Visual progress bars for cloud uploads/downloads
  - Byte transfer display, speed, ETA prediction
  - Color-coded Unicode block progress
- Checksum verification with progress bar for large files
- Spinner for indeterminate operations (unknown size)
- New types: NewSchollzBar(), NewSchollzBarItems(), NewSchollzSpinner()
- Progress Writer() method for io.Copy integration
2026-01-14 16:07:04 +01:00

626 lines
14 KiB
Go
Executable File

package progress
import (
"fmt"
"io"
"os"
"strings"
"time"
"github.com/schollz/progressbar/v3"
)
// Indicator represents a progress indicator interface
type Indicator interface {
Start(message string)
Update(message string)
Complete(message string)
Fail(message string)
Stop()
SetEstimator(estimator *ETAEstimator)
}
// Spinner creates a spinning progress indicator
type Spinner struct {
writer io.Writer
message string
active bool
frames []string
interval time.Duration
stopCh chan bool
estimator *ETAEstimator
}
// NewSpinner creates a new spinner progress indicator
func NewSpinner() *Spinner {
return &Spinner{
writer: os.Stdout,
frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
interval: 80 * time.Millisecond,
stopCh: make(chan bool, 1),
}
}
// Start begins the spinner with a message
func (s *Spinner) Start(message string) {
s.message = message
s.active = true
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
i := 0
lastMessage := ""
for {
select {
case <-s.stopCh:
return
case <-ticker.C:
if s.active {
displayMsg := s.message
// Add ETA info if estimator is available
if s.estimator != nil {
displayMsg = s.estimator.GetFullStatus(s.message)
}
currentFrame := fmt.Sprintf("%s %s", s.frames[i%len(s.frames)], displayMsg)
if s.message != lastMessage {
// Print new line for new messages
fmt.Fprintf(s.writer, "\n%s", currentFrame)
lastMessage = s.message
} else {
// Update in place for same message
fmt.Fprintf(s.writer, "\r%s", currentFrame)
}
i++
}
}
}
}()
}
// SetEstimator sets an ETA estimator for this spinner
func (s *Spinner) SetEstimator(estimator *ETAEstimator) {
s.estimator = estimator
}
// Update changes the spinner message
func (s *Spinner) Update(message string) {
s.message = message
}
// Complete stops the spinner with a success message
func (s *Spinner) Complete(message string) {
s.Stop()
fmt.Fprintf(s.writer, "\n[OK] %s\n", message)
}
// Fail stops the spinner with a failure message
func (s *Spinner) Fail(message string) {
s.Stop()
fmt.Fprintf(s.writer, "\n[FAIL] %s\n", message)
}
// Stop stops the spinner
func (s *Spinner) Stop() {
if s.active {
s.active = false
s.stopCh <- true
fmt.Fprint(s.writer, "\n") // New line instead of clearing
}
}
// Dots creates a dots progress indicator
type Dots struct {
writer io.Writer
message string
active bool
stopCh chan bool
}
// NewDots creates a new dots progress indicator
func NewDots() *Dots {
return &Dots{
writer: os.Stdout,
stopCh: make(chan bool, 1),
}
}
// Start begins the dots indicator
func (d *Dots) Start(message string) {
d.message = message
d.active = true
fmt.Fprint(d.writer, message)
go func() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
count := 0
for {
select {
case <-d.stopCh:
return
case <-ticker.C:
if d.active {
fmt.Fprint(d.writer, ".")
count++
if count%3 == 0 {
// Reset dots
fmt.Fprint(d.writer, "\r"+d.message)
}
}
}
}
}()
}
// Update changes the dots message
func (d *Dots) Update(message string) {
d.message = message
if d.active {
fmt.Fprintf(d.writer, "\n%s", message)
}
}
// Complete stops the dots with a success message
func (d *Dots) Complete(message string) {
d.Stop()
fmt.Fprintf(d.writer, " [OK] %s\n", message)
}
// Fail stops the dots with a failure message
func (d *Dots) Fail(message string) {
d.Stop()
fmt.Fprintf(d.writer, " [FAIL] %s\n", message)
}
// Stop stops the dots indicator
func (d *Dots) Stop() {
if d.active {
d.active = false
d.stopCh <- true
}
}
// SetEstimator is a no-op for dots (doesn't support ETA display)
func (d *Dots) SetEstimator(estimator *ETAEstimator) {
// Dots indicator doesn't support ETA display
}
// ProgressBar creates a visual progress bar
type ProgressBar struct {
writer io.Writer
message string
total int
current int
width int
active bool
stopCh chan bool
}
// NewProgressBar creates a new progress bar
func NewProgressBar(total int) *ProgressBar {
return &ProgressBar{
writer: os.Stdout,
total: total,
width: 40,
stopCh: make(chan bool, 1),
}
}
// Start begins the progress bar
func (p *ProgressBar) Start(message string) {
p.message = message
p.active = true
p.current = 0
p.render()
}
// Update advances the progress bar
func (p *ProgressBar) Update(message string) {
if p.current < p.total {
p.current++
}
p.message = message
p.render()
}
// SetProgress sets specific progress value
func (p *ProgressBar) SetProgress(current int, message string) {
p.current = current
p.message = message
p.render()
}
// Complete finishes the progress bar
func (p *ProgressBar) Complete(message string) {
p.current = p.total
p.message = message
p.render()
fmt.Fprintf(p.writer, " [OK] %s\n", message)
p.Stop()
}
// Fail stops the progress bar with failure
func (p *ProgressBar) Fail(message string) {
p.render()
fmt.Fprintf(p.writer, " [FAIL] %s\n", message)
p.Stop()
}
// Stop stops the progress bar
func (p *ProgressBar) Stop() {
p.active = false
}
// SetEstimator is a no-op for progress bar (has its own progress tracking)
func (p *ProgressBar) SetEstimator(estimator *ETAEstimator) {
// Progress bar has its own progress tracking
}
// render draws the progress bar
func (p *ProgressBar) render() {
if !p.active {
return
}
percent := float64(p.current) / float64(p.total)
filled := int(percent * float64(p.width))
bar := strings.Repeat("█", filled) + strings.Repeat("░", p.width-filled)
fmt.Fprintf(p.writer, "\n%s [%s] %d%%", p.message, bar, int(percent*100))
}
// Static creates a simple static progress indicator
type Static struct {
writer io.Writer
}
// NewStatic creates a new static progress indicator
func NewStatic() *Static {
return &Static{
writer: os.Stdout,
}
}
// Start shows the initial message
func (s *Static) Start(message string) {
fmt.Fprintf(s.writer, "→ %s", message)
}
// Update shows an update message
func (s *Static) Update(message string) {
fmt.Fprintf(s.writer, " - %s", message)
}
// Complete shows completion message
func (s *Static) Complete(message string) {
fmt.Fprintf(s.writer, " [OK] %s\n", message)
}
// Fail shows failure message
func (s *Static) Fail(message string) {
fmt.Fprintf(s.writer, " [FAIL] %s\n", message)
}
// Stop does nothing for static indicator
func (s *Static) Stop() {
// No-op for static indicator
}
// SetEstimator is a no-op for static indicator
func (s *Static) SetEstimator(estimator *ETAEstimator) {
// Static indicator doesn't support ETA display
}
// LineByLine creates a line-by-line progress indicator
type LineByLine struct {
writer io.Writer
silent bool
estimator *ETAEstimator
}
// NewLineByLine creates a new line-by-line progress indicator
func NewLineByLine() *LineByLine {
return &LineByLine{
writer: os.Stdout,
silent: false,
}
}
// Light creates a minimal progress indicator with just essential status
type Light struct {
writer io.Writer
silent bool
}
// NewLight creates a new light progress indicator
func NewLight() *Light {
return &Light{
writer: os.Stdout,
silent: false,
}
}
// NewQuietLineByLine creates a quiet line-by-line progress indicator
func NewQuietLineByLine() *LineByLine {
return &LineByLine{
writer: os.Stdout,
silent: true,
}
}
// Start shows the initial message
func (l *LineByLine) Start(message string) {
displayMsg := message
if l.estimator != nil {
displayMsg = l.estimator.GetFullStatus(message)
}
fmt.Fprintf(l.writer, "\n[SYNC] %s\n", displayMsg)
}
// Update shows an update message
func (l *LineByLine) Update(message string) {
if !l.silent {
displayMsg := message
if l.estimator != nil {
displayMsg = l.estimator.GetFullStatus(message)
}
fmt.Fprintf(l.writer, " %s\n", displayMsg)
}
}
// SetEstimator sets an ETA estimator for this indicator
func (l *LineByLine) SetEstimator(estimator *ETAEstimator) {
l.estimator = estimator
}
// Complete shows completion message
func (l *LineByLine) Complete(message string) {
fmt.Fprintf(l.writer, "[OK] %s\n\n", message)
}
// Fail shows failure message
func (l *LineByLine) Fail(message string) {
fmt.Fprintf(l.writer, "[FAIL] %s\n\n", message)
}
// Stop does nothing for line-by-line (no cleanup needed)
func (l *LineByLine) Stop() {
// No cleanup needed for line-by-line
}
// Light indicator methods - minimal output
func (l *Light) Start(message string) {
if !l.silent {
fmt.Fprintf(l.writer, "> %s\n", message)
}
}
func (l *Light) Update(message string) {
if !l.silent {
fmt.Fprintf(l.writer, " %s\n", message)
}
}
func (l *Light) Complete(message string) {
if !l.silent {
fmt.Fprintf(l.writer, "[OK] %s\n", message)
}
}
func (l *Light) Fail(message string) {
if !l.silent {
fmt.Fprintf(l.writer, "[FAIL] %s\n", message)
}
}
func (l *Light) Stop() {
// No cleanup needed for light indicator
}
// SetEstimator is a no-op for light indicator
func (l *Light) SetEstimator(estimator *ETAEstimator) {
// Light indicator doesn't support ETA display
}
// NewIndicator creates an appropriate progress indicator based on environment
func NewIndicator(interactive bool, indicatorType string) Indicator {
if !interactive {
return NewLineByLine() // Use line-by-line for non-interactive mode
}
switch indicatorType {
case "spinner":
return NewSpinner()
case "dots":
return NewDots()
case "bar":
return NewProgressBar(100) // Default to 100 steps
case "schollz":
return NewSchollzBarItems(100, "Progress")
case "line":
return NewLineByLine()
case "light":
return NewLight()
default:
return NewLineByLine() // Default to line-by-line for better compatibility
}
}
// NullIndicator is a no-op indicator that produces no output (for TUI mode)
type NullIndicator struct{}
// NewNullIndicator creates an indicator that does nothing
func NewNullIndicator() *NullIndicator {
return &NullIndicator{}
}
func (n *NullIndicator) Start(message string) {}
func (n *NullIndicator) Update(message string) {}
func (n *NullIndicator) Complete(message string) {}
func (n *NullIndicator) Fail(message string) {}
func (n *NullIndicator) Stop() {}
func (n *NullIndicator) SetEstimator(estimator *ETAEstimator) {}
// SchollzBar wraps schollz/progressbar for enhanced progress display
// Ideal for byte-based operations like archive extraction and file transfers
type SchollzBar struct {
bar *progressbar.ProgressBar
message string
total int64
estimator *ETAEstimator
}
// NewSchollzBar creates a new schollz progressbar with byte-based progress
func NewSchollzBar(total int64, description string) *SchollzBar {
bar := progressbar.NewOptions64(
total,
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(40),
progressbar.OptionSetDescription(description),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]█[reset]",
SaucerHead: "[green]▌[reset]",
SaucerPadding: "░",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionShowCount(),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionClearOnFinish(),
)
return &SchollzBar{
bar: bar,
message: description,
total: total,
}
}
// NewSchollzBarItems creates a progressbar for item counts (not bytes)
func NewSchollzBarItems(total int, description string) *SchollzBar {
bar := progressbar.NewOptions(
total,
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWidth(40),
progressbar.OptionSetDescription(description),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[cyan]█[reset]",
SaucerHead: "[cyan]▌[reset]",
SaucerPadding: "░",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionClearOnFinish(),
)
return &SchollzBar{
bar: bar,
message: description,
total: int64(total),
}
}
// NewSchollzSpinner creates an indeterminate spinner for unknown-length operations
func NewSchollzSpinner(description string) *SchollzBar {
bar := progressbar.NewOptions(
-1, // Indeterminate
progressbar.OptionEnableColorCodes(true),
progressbar.OptionSetWidth(40),
progressbar.OptionSetDescription(description),
progressbar.OptionSpinnerType(14), // Braille spinner
progressbar.OptionFullWidth(),
)
return &SchollzBar{
bar: bar,
message: description,
total: -1,
}
}
// Start initializes the progress bar (Indicator interface)
func (s *SchollzBar) Start(message string) {
s.message = message
s.bar.Describe(message)
}
// Update updates the description (Indicator interface)
func (s *SchollzBar) Update(message string) {
s.message = message
s.bar.Describe(message)
}
// Add adds bytes/items to the progress
func (s *SchollzBar) Add(n int) error {
return s.bar.Add(n)
}
// Add64 adds bytes to the progress (for large files)
func (s *SchollzBar) Add64(n int64) error {
return s.bar.Add64(n)
}
// Set sets the current progress value
func (s *SchollzBar) Set(n int) error {
return s.bar.Set(n)
}
// Set64 sets the current progress value (for large files)
func (s *SchollzBar) Set64(n int64) error {
return s.bar.Set64(n)
}
// ChangeMax updates the maximum value
func (s *SchollzBar) ChangeMax(max int) {
s.bar.ChangeMax(max)
s.total = int64(max)
}
// ChangeMax64 updates the maximum value (for large files)
func (s *SchollzBar) ChangeMax64(max int64) {
s.bar.ChangeMax64(max)
s.total = max
}
// Complete finishes with success (Indicator interface)
func (s *SchollzBar) Complete(message string) {
_ = s.bar.Finish()
fmt.Printf("\n[green][OK][reset] %s\n", message)
}
// Fail finishes with failure (Indicator interface)
func (s *SchollzBar) Fail(message string) {
_ = s.bar.Clear()
fmt.Printf("\n[red][FAIL][reset] %s\n", message)
}
// Stop stops the progress bar (Indicator interface)
func (s *SchollzBar) Stop() {
_ = s.bar.Clear()
}
// SetEstimator is a no-op (schollz has built-in ETA)
func (s *SchollzBar) SetEstimator(estimator *ETAEstimator) {
s.estimator = estimator
}
// Writer returns an io.Writer that updates progress as data is written
// Useful for wrapping readers/writers in copy operations
func (s *SchollzBar) Writer() io.Writer {
return s.bar
}
// Finish marks the progress as complete
func (s *SchollzBar) Finish() error {
return s.bar.Finish()
}