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() }