- Windows-compatible colors via native console API - Color helper functions: Success(), Error(), Warning(), Info() - Text styling: Header(), Dim(), Bold(), Green(), Red(), Yellow(), Cyan() - Logger CleanFormatter uses fatih/color instead of raw ANSI - All progress indicators use colored [OK]/[FAIL] status - Automatic color detection for non-TTY environments
648 lines
15 KiB
Go
Executable File
648 lines
15 KiB
Go
Executable File
package progress
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/schollz/progressbar/v3"
|
|
)
|
|
|
|
// Color printers for progress indicators
|
|
var (
|
|
okColor = color.New(color.FgGreen, color.Bold)
|
|
failColor = color.New(color.FgRed, color.Bold)
|
|
warnColor = color.New(color.FgYellow, color.Bold)
|
|
)
|
|
|
|
// 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()
|
|
okColor.Fprint(s.writer, "[OK] ")
|
|
fmt.Fprintln(s.writer, message)
|
|
}
|
|
|
|
// Fail stops the spinner with a failure message
|
|
func (s *Spinner) Fail(message string) {
|
|
s.Stop()
|
|
failColor.Fprint(s.writer, "[FAIL] ")
|
|
fmt.Fprintln(s.writer, 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()
|
|
okColor.Fprint(d.writer, " [OK] ")
|
|
fmt.Fprintln(d.writer, message)
|
|
}
|
|
|
|
// Fail stops the dots with a failure message
|
|
func (d *Dots) Fail(message string) {
|
|
d.Stop()
|
|
failColor.Fprint(d.writer, " [FAIL] ")
|
|
fmt.Fprintln(d.writer, 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()
|
|
okColor.Fprint(p.writer, " [OK] ")
|
|
fmt.Fprintln(p.writer, message)
|
|
p.Stop()
|
|
}
|
|
|
|
// Fail stops the progress bar with failure
|
|
func (p *ProgressBar) Fail(message string) {
|
|
p.render()
|
|
failColor.Fprint(p.writer, " [FAIL] ")
|
|
fmt.Fprintln(p.writer, 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) {
|
|
okColor.Fprint(s.writer, " [OK] ")
|
|
fmt.Fprintln(s.writer, message)
|
|
}
|
|
|
|
// Fail shows failure message
|
|
func (s *Static) Fail(message string) {
|
|
failColor.Fprint(s.writer, " [FAIL] ")
|
|
fmt.Fprintln(s.writer, 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) {
|
|
okColor.Fprint(l.writer, "[OK] ")
|
|
fmt.Fprintf(l.writer, "%s\n\n", message)
|
|
}
|
|
|
|
// Fail shows failure message
|
|
func (l *LineByLine) Fail(message string) {
|
|
failColor.Fprint(l.writer, "[FAIL] ")
|
|
fmt.Fprintf(l.writer, "%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 {
|
|
okColor.Fprint(l.writer, "[OK] ")
|
|
fmt.Fprintln(l.writer, message)
|
|
}
|
|
}
|
|
|
|
func (l *Light) Fail(message string) {
|
|
if !l.silent {
|
|
failColor.Fprint(l.writer, "[FAIL] ")
|
|
fmt.Fprintln(l.writer, 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()
|
|
okColor.Print("[OK] ")
|
|
fmt.Println(message)
|
|
}
|
|
|
|
// Fail finishes with failure (Indicator interface)
|
|
func (s *SchollzBar) Fail(message string) {
|
|
_ = s.bar.Clear()
|
|
failColor.Print("[FAIL] ")
|
|
fmt.Println(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()
|
|
}
|