package sync import ( "fmt" "io" "os" "time" "github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/text" ) // ProgressReporter interface for sync progress reporting type ProgressReporter interface { // Phase tracking StartPhase(name string, total int64) UpdatePhase(current int64, message string) CompletePhase() // Overall status SetStatus(message string) Done() } // NoOpProgress is a no-op progress reporter for non-interactive scenarios type NoOpProgress struct{} func (p *NoOpProgress) StartPhase(name string, total int64) {} func (p *NoOpProgress) UpdatePhase(current int64, message string) {} func (p *NoOpProgress) CompletePhase() {} func (p *NoOpProgress) SetStatus(message string) {} func (p *NoOpProgress) Done() {} // InteractiveProgress provides visual progress indication using go-pretty type InteractiveProgress struct { writer progress.Writer currentTracker *progress.Tracker baseMessage string // Store base message for current phase startTime time.Time output io.Writer } // NewInteractiveProgress creates a new interactive progress reporter func NewInteractiveProgress(output io.Writer) *InteractiveProgress { pw := progress.NewWriter() pw.SetOutputWriter(output) // Configure style pw.SetStyle(progress.StyleDefault) pw.Style().Colors = progress.StyleColors{ Message: text.Colors{text.FgHiWhite}, Percent: text.Colors{text.FgHiCyan}, Stats: text.Colors{text.FgHiBlack}, Time: text.Colors{text.FgHiBlack}, Tracker: text.Colors{text.FgHiWhite}, Value: text.Colors{text.FgCyan}, } pw.Style().Visibility.ETA = false pw.Style().Visibility.ETAOverall = false pw.Style().Visibility.Speed = false pw.Style().Visibility.SpeedOverall = false pw.Style().Visibility.Value = true pw.Style().Visibility.Percentage = true pw.Style().Options.PercentFormat = "%4.1f%%" // Start rendering go pw.Render() return &InteractiveProgress{ writer: pw, startTime: time.Now(), output: output, } } // StartPhase begins a new progress phase func (p *InteractiveProgress) StartPhase(name string, total int64) { // Complete previous phase if exists if p.currentTracker != nil { p.currentTracker.MarkAsDone() } // Create new tracker tracker := &progress.Tracker{ Message: name, Total: total, Units: progress.Units{}, } p.writer.AppendTracker(tracker) p.currentTracker = tracker p.baseMessage = name // Store base message } // UpdatePhase updates the current phase progress func (p *InteractiveProgress) UpdatePhase(current int64, message string) { if p.currentTracker != nil { p.currentTracker.SetValue(current) if message != "" { // Use base message + status to avoid appending repeatedly p.currentTracker.UpdateMessage(fmt.Sprintf("%s - %s", p.baseMessage, message)) } } } // CompletePhase marks the current phase as complete func (p *InteractiveProgress) CompletePhase() { if p.currentTracker != nil { p.currentTracker.MarkAsDone() p.currentTracker = nil } } // SetStatus updates the overall status message func (p *InteractiveProgress) SetStatus(message string) { // For status updates without a phase, we can use UpdateMessage on current tracker if p.currentTracker != nil { p.currentTracker.UpdateMessage(message) } } // Done completes all progress and cleans up func (p *InteractiveProgress) Done() { // Mark any remaining tracker as done if p.currentTracker != nil { p.currentTracker.MarkAsDone() p.currentTracker = nil } // Stop the writer (this waits for all trackers to finish) p.writer.Stop() // Add a newline for clean separation from subsequent output fmt.Fprintln(p.output) } // formatDuration formats a duration in a human-readable way func formatDuration(d time.Duration) string { if d < time.Second { return fmt.Sprintf("%dms", d.Milliseconds()) } if d < time.Minute { return fmt.Sprintf("%.1fs", d.Seconds()) } minutes := int(d.Minutes()) seconds := int(d.Seconds()) % 60 return fmt.Sprintf("%dm%ds", minutes, seconds) } // ShouldShowProgress determines if progress should be shown based on environment func ShouldShowProgress(quiet bool) bool { if quiet { return false } // Check if output is a terminal fileInfo, err := os.Stdout.Stat() if err != nil { return false } // Check if it's a character device (terminal) return (fileInfo.Mode() & os.ModeCharDevice) != 0 }