1c3186a342
- Implement ProgressReporter interface with InteractiveProgress and NoOpProgress - Add real-time progress bars using go-pretty/progress library - Track 6 sync phases: connection test, queue push, pull, parse, apply, and server push - Add --quiet flag to suppress progress output - Auto-detect TTY to disable progress when piped/redirected - Show task-level progress during apply phase with descriptions - Display percentage complete and elapsed time for each phase
165 lines
4.4 KiB
Go
165 lines
4.4 KiB
Go
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
|
|
}
|