Initial commit: Database Backup Tool v1.1.0
- PostgreSQL and MySQL support - Interactive TUI with fixed menu navigation - Line-by-line progress display - CPU-aware parallel processing - Cross-platform build support - Configuration settings menu - Silent mode for TUI operations
This commit is contained in:
427
internal/progress/detailed.go
Normal file
427
internal/progress/detailed.go
Normal file
@ -0,0 +1,427 @@
|
||||
package progress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DetailedReporter provides comprehensive progress reporting with timestamps and status
|
||||
type DetailedReporter struct {
|
||||
mu sync.RWMutex
|
||||
operations []OperationStatus
|
||||
startTime time.Time
|
||||
indicator Indicator
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// OperationStatus represents the status of a backup/restore operation
|
||||
type OperationStatus struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "backup", "restore", "verify"
|
||||
Status string `json:"status"` // "running", "completed", "failed"
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time,omitempty"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Progress int `json:"progress"` // 0-100
|
||||
Message string `json:"message"`
|
||||
Details map[string]string `json:"details"`
|
||||
Steps []StepStatus `json:"steps"`
|
||||
BytesTotal int64 `json:"bytes_total"`
|
||||
BytesDone int64 `json:"bytes_done"`
|
||||
FilesTotal int `json:"files_total"`
|
||||
FilesDone int `json:"files_done"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// StepStatus represents individual steps within an operation
|
||||
type StepStatus struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time,omitempty"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Logger interface for detailed reporting
|
||||
type Logger interface {
|
||||
Info(msg string, args ...any)
|
||||
Warn(msg string, args ...any)
|
||||
Error(msg string, args ...any)
|
||||
Debug(msg string, args ...any)
|
||||
}
|
||||
|
||||
// NewDetailedReporter creates a new detailed progress reporter
|
||||
func NewDetailedReporter(indicator Indicator, logger Logger) *DetailedReporter {
|
||||
return &DetailedReporter{
|
||||
operations: make([]OperationStatus, 0),
|
||||
indicator: indicator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// StartOperation begins tracking a new operation
|
||||
func (dr *DetailedReporter) StartOperation(id, name, opType string) *OperationTracker {
|
||||
dr.mu.Lock()
|
||||
defer dr.mu.Unlock()
|
||||
|
||||
operation := OperationStatus{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Type: opType,
|
||||
Status: "running",
|
||||
StartTime: time.Now(),
|
||||
Progress: 0,
|
||||
Details: make(map[string]string),
|
||||
Steps: make([]StepStatus, 0),
|
||||
}
|
||||
|
||||
dr.operations = append(dr.operations, operation)
|
||||
|
||||
if dr.startTime.IsZero() {
|
||||
dr.startTime = time.Now()
|
||||
}
|
||||
|
||||
// Start visual indicator
|
||||
if dr.indicator != nil {
|
||||
dr.indicator.Start(fmt.Sprintf("Starting %s: %s", opType, name))
|
||||
}
|
||||
|
||||
// Log operation start
|
||||
dr.logger.Info("Operation started",
|
||||
"id", id,
|
||||
"name", name,
|
||||
"type", opType,
|
||||
"timestamp", operation.StartTime.Format(time.RFC3339))
|
||||
|
||||
return &OperationTracker{
|
||||
reporter: dr,
|
||||
operationID: id,
|
||||
}
|
||||
}
|
||||
|
||||
// OperationTracker provides methods to update operation progress
|
||||
type OperationTracker struct {
|
||||
reporter *DetailedReporter
|
||||
operationID string
|
||||
}
|
||||
|
||||
// UpdateProgress updates the progress of the operation
|
||||
func (ot *OperationTracker) UpdateProgress(progress int, message string) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].Progress = progress
|
||||
ot.reporter.operations[i].Message = message
|
||||
|
||||
// Update visual indicator
|
||||
if ot.reporter.indicator != nil {
|
||||
progressMsg := fmt.Sprintf("[%d%%] %s", progress, message)
|
||||
ot.reporter.indicator.Update(progressMsg)
|
||||
}
|
||||
|
||||
// Log progress update
|
||||
ot.reporter.logger.Debug("Progress update",
|
||||
"operation_id", ot.operationID,
|
||||
"progress", progress,
|
||||
"message", message,
|
||||
"timestamp", time.Now().Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddStep adds a new step to the operation
|
||||
func (ot *OperationTracker) AddStep(name, message string) *StepTracker {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
step := StepStatus{
|
||||
Name: name,
|
||||
Status: "running",
|
||||
StartTime: time.Now(),
|
||||
Message: message,
|
||||
}
|
||||
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].Steps = append(ot.reporter.operations[i].Steps, step)
|
||||
|
||||
// Log step start
|
||||
ot.reporter.logger.Info("Step started",
|
||||
"operation_id", ot.operationID,
|
||||
"step", name,
|
||||
"message", message,
|
||||
"timestamp", step.StartTime.Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &StepTracker{
|
||||
reporter: ot.reporter,
|
||||
operationID: ot.operationID,
|
||||
stepName: name,
|
||||
}
|
||||
}
|
||||
|
||||
// SetDetails adds metadata to the operation
|
||||
func (ot *OperationTracker) SetDetails(key, value string) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].Details[key] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetFileProgress updates file-based progress
|
||||
func (ot *OperationTracker) SetFileProgress(filesDone, filesTotal int) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].FilesDone = filesDone
|
||||
ot.reporter.operations[i].FilesTotal = filesTotal
|
||||
|
||||
if filesTotal > 0 {
|
||||
progress := (filesDone * 100) / filesTotal
|
||||
ot.reporter.operations[i].Progress = progress
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetByteProgress updates byte-based progress
|
||||
func (ot *OperationTracker) SetByteProgress(bytesDone, bytesTotal int64) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].BytesDone = bytesDone
|
||||
ot.reporter.operations[i].BytesTotal = bytesTotal
|
||||
|
||||
if bytesTotal > 0 {
|
||||
progress := int((bytesDone * 100) / bytesTotal)
|
||||
ot.reporter.operations[i].Progress = progress
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete marks the operation as completed
|
||||
func (ot *OperationTracker) Complete(message string) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].Status = "completed"
|
||||
ot.reporter.operations[i].Progress = 100
|
||||
ot.reporter.operations[i].EndTime = &now
|
||||
ot.reporter.operations[i].Duration = now.Sub(ot.reporter.operations[i].StartTime)
|
||||
ot.reporter.operations[i].Message = message
|
||||
|
||||
// Complete visual indicator
|
||||
if ot.reporter.indicator != nil {
|
||||
ot.reporter.indicator.Complete(fmt.Sprintf("✅ %s", message))
|
||||
}
|
||||
|
||||
// Log completion with duration
|
||||
ot.reporter.logger.Info("Operation completed",
|
||||
"operation_id", ot.operationID,
|
||||
"message", message,
|
||||
"duration", ot.reporter.operations[i].Duration.String(),
|
||||
"timestamp", now.Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fail marks the operation as failed
|
||||
func (ot *OperationTracker) Fail(err error) {
|
||||
ot.reporter.mu.Lock()
|
||||
defer ot.reporter.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for i := range ot.reporter.operations {
|
||||
if ot.reporter.operations[i].ID == ot.operationID {
|
||||
ot.reporter.operations[i].Status = "failed"
|
||||
ot.reporter.operations[i].EndTime = &now
|
||||
ot.reporter.operations[i].Duration = now.Sub(ot.reporter.operations[i].StartTime)
|
||||
ot.reporter.operations[i].Message = err.Error()
|
||||
ot.reporter.operations[i].Errors = append(ot.reporter.operations[i].Errors, err.Error())
|
||||
|
||||
// Fail visual indicator
|
||||
if ot.reporter.indicator != nil {
|
||||
ot.reporter.indicator.Fail(fmt.Sprintf("❌ %s", err.Error()))
|
||||
}
|
||||
|
||||
// Log failure
|
||||
ot.reporter.logger.Error("Operation failed",
|
||||
"operation_id", ot.operationID,
|
||||
"error", err.Error(),
|
||||
"duration", ot.reporter.operations[i].Duration.String(),
|
||||
"timestamp", now.Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StepTracker manages individual step progress
|
||||
type StepTracker struct {
|
||||
reporter *DetailedReporter
|
||||
operationID string
|
||||
stepName string
|
||||
}
|
||||
|
||||
// Complete marks the step as completed
|
||||
func (st *StepTracker) Complete(message string) {
|
||||
st.reporter.mu.Lock()
|
||||
defer st.reporter.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for i := range st.reporter.operations {
|
||||
if st.reporter.operations[i].ID == st.operationID {
|
||||
for j := range st.reporter.operations[i].Steps {
|
||||
if st.reporter.operations[i].Steps[j].Name == st.stepName {
|
||||
st.reporter.operations[i].Steps[j].Status = "completed"
|
||||
st.reporter.operations[i].Steps[j].EndTime = &now
|
||||
st.reporter.operations[i].Steps[j].Duration = now.Sub(st.reporter.operations[i].Steps[j].StartTime)
|
||||
st.reporter.operations[i].Steps[j].Message = message
|
||||
|
||||
// Log step completion
|
||||
st.reporter.logger.Info("Step completed",
|
||||
"operation_id", st.operationID,
|
||||
"step", st.stepName,
|
||||
"message", message,
|
||||
"duration", st.reporter.operations[i].Steps[j].Duration.String(),
|
||||
"timestamp", now.Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fail marks the step as failed
|
||||
func (st *StepTracker) Fail(err error) {
|
||||
st.reporter.mu.Lock()
|
||||
defer st.reporter.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for i := range st.reporter.operations {
|
||||
if st.reporter.operations[i].ID == st.operationID {
|
||||
for j := range st.reporter.operations[i].Steps {
|
||||
if st.reporter.operations[i].Steps[j].Name == st.stepName {
|
||||
st.reporter.operations[i].Steps[j].Status = "failed"
|
||||
st.reporter.operations[i].Steps[j].EndTime = &now
|
||||
st.reporter.operations[i].Steps[j].Duration = now.Sub(st.reporter.operations[i].Steps[j].StartTime)
|
||||
st.reporter.operations[i].Steps[j].Message = err.Error()
|
||||
|
||||
// Log step failure
|
||||
st.reporter.logger.Error("Step failed",
|
||||
"operation_id", st.operationID,
|
||||
"step", st.stepName,
|
||||
"error", err.Error(),
|
||||
"duration", st.reporter.operations[i].Steps[j].Duration.String(),
|
||||
"timestamp", now.Format(time.RFC3339))
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetOperationStatus returns the current status of an operation
|
||||
func (dr *DetailedReporter) GetOperationStatus(id string) *OperationStatus {
|
||||
dr.mu.RLock()
|
||||
defer dr.mu.RUnlock()
|
||||
|
||||
for _, op := range dr.operations {
|
||||
if op.ID == id {
|
||||
return &op
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllOperations returns all tracked operations
|
||||
func (dr *DetailedReporter) GetAllOperations() []OperationStatus {
|
||||
dr.mu.RLock()
|
||||
defer dr.mu.RUnlock()
|
||||
|
||||
return append([]OperationStatus(nil), dr.operations...)
|
||||
}
|
||||
|
||||
// GetSummary returns a summary of all operations
|
||||
func (dr *DetailedReporter) GetSummary() OperationSummary {
|
||||
dr.mu.RLock()
|
||||
defer dr.mu.RUnlock()
|
||||
|
||||
summary := OperationSummary{
|
||||
TotalOperations: len(dr.operations),
|
||||
CompletedOperations: 0,
|
||||
FailedOperations: 0,
|
||||
RunningOperations: 0,
|
||||
TotalDuration: time.Since(dr.startTime),
|
||||
}
|
||||
|
||||
for _, op := range dr.operations {
|
||||
switch op.Status {
|
||||
case "completed":
|
||||
summary.CompletedOperations++
|
||||
case "failed":
|
||||
summary.FailedOperations++
|
||||
case "running":
|
||||
summary.RunningOperations++
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// OperationSummary provides overall statistics
|
||||
type OperationSummary struct {
|
||||
TotalOperations int `json:"total_operations"`
|
||||
CompletedOperations int `json:"completed_operations"`
|
||||
FailedOperations int `json:"failed_operations"`
|
||||
RunningOperations int `json:"running_operations"`
|
||||
TotalDuration time.Duration `json:"total_duration"`
|
||||
}
|
||||
|
||||
// FormatSummary returns a formatted string representation of the summary
|
||||
func (os *OperationSummary) FormatSummary() string {
|
||||
return fmt.Sprintf(
|
||||
"📊 Operations Summary:\n"+
|
||||
" Total: %d | Completed: %d | Failed: %d | Running: %d\n"+
|
||||
" Total Duration: %s",
|
||||
os.TotalOperations,
|
||||
os.CompletedOperations,
|
||||
os.FailedOperations,
|
||||
os.RunningOperations,
|
||||
formatDuration(os.TotalDuration))
|
||||
}
|
||||
|
||||
// formatDuration formats a duration in a human-readable way
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
||||
} else if d < time.Hour {
|
||||
return fmt.Sprintf("%.1fm", d.Minutes())
|
||||
}
|
||||
return fmt.Sprintf("%.1fh", d.Hours())
|
||||
}
|
||||
398
internal/progress/progress.go
Normal file
398
internal/progress/progress.go
Normal file
@ -0,0 +1,398 @@
|
||||
package progress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Indicator represents a progress indicator interface
|
||||
type Indicator interface {
|
||||
Start(message string)
|
||||
Update(message string)
|
||||
Complete(message string)
|
||||
Fail(message string)
|
||||
Stop()
|
||||
}
|
||||
|
||||
// Spinner creates a spinning progress indicator
|
||||
type Spinner struct {
|
||||
writer io.Writer
|
||||
message string
|
||||
active bool
|
||||
frames []string
|
||||
interval time.Duration
|
||||
stopCh chan bool
|
||||
}
|
||||
|
||||
// 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() {
|
||||
i := 0
|
||||
lastMessage := ""
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
return
|
||||
default:
|
||||
if s.active {
|
||||
currentFrame := fmt.Sprintf("%s %s", s.frames[i%len(s.frames)], s.message)
|
||||
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++
|
||||
time.Sleep(s.interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 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✅ %s\n", message)
|
||||
}
|
||||
|
||||
// Fail stops the spinner with a failure message
|
||||
func (s *Spinner) Fail(message string) {
|
||||
s.Stop()
|
||||
fmt.Fprintf(s.writer, "\n❌ %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() {
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case <-d.stopCh:
|
||||
return
|
||||
default:
|
||||
if d.active {
|
||||
fmt.Fprint(d.writer, ".")
|
||||
count++
|
||||
if count%3 == 0 {
|
||||
// Reset dots
|
||||
fmt.Fprint(d.writer, "\r"+d.message)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 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, " ✅ %s\n", message)
|
||||
}
|
||||
|
||||
// Fail stops the dots with a failure message
|
||||
func (d *Dots) Fail(message string) {
|
||||
d.Stop()
|
||||
fmt.Fprintf(d.writer, " ❌ %s\n", message)
|
||||
}
|
||||
|
||||
// Stop stops the dots indicator
|
||||
func (d *Dots) Stop() {
|
||||
if d.active {
|
||||
d.active = false
|
||||
d.stopCh <- true
|
||||
}
|
||||
}
|
||||
|
||||
// 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, " ✅ %s\n", message)
|
||||
p.Stop()
|
||||
}
|
||||
|
||||
// Fail stops the progress bar with failure
|
||||
func (p *ProgressBar) Fail(message string) {
|
||||
p.render()
|
||||
fmt.Fprintf(p.writer, " ❌ %s\n", message)
|
||||
p.Stop()
|
||||
}
|
||||
|
||||
// Stop stops the progress bar
|
||||
func (p *ProgressBar) Stop() {
|
||||
p.active = false
|
||||
}
|
||||
|
||||
// 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, " ✅ %s\n", message)
|
||||
}
|
||||
|
||||
// Fail shows failure message
|
||||
func (s *Static) Fail(message string) {
|
||||
fmt.Fprintf(s.writer, " ❌ %s\n", message)
|
||||
}
|
||||
|
||||
// Stop does nothing for static indicator
|
||||
func (s *Static) Stop() {
|
||||
// No-op for static indicator
|
||||
}
|
||||
|
||||
// LineByLine creates a line-by-line progress indicator
|
||||
type LineByLine struct {
|
||||
writer io.Writer
|
||||
silent bool
|
||||
}
|
||||
|
||||
// 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) {
|
||||
fmt.Fprintf(l.writer, "\n🔄 %s\n", message)
|
||||
}
|
||||
|
||||
// Update shows an update message
|
||||
func (l *LineByLine) Update(message string) {
|
||||
if !l.silent {
|
||||
fmt.Fprintf(l.writer, " %s\n", message)
|
||||
}
|
||||
}
|
||||
|
||||
// Complete shows completion message
|
||||
func (l *LineByLine) Complete(message string) {
|
||||
fmt.Fprintf(l.writer, "✅ %s\n\n", message)
|
||||
}
|
||||
|
||||
// Fail shows failure message
|
||||
func (l *LineByLine) Fail(message string) {
|
||||
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 {
|
||||
fmt.Fprintf(l.writer, "✓ %s\n", message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Light) Fail(message string) {
|
||||
if !l.silent {
|
||||
fmt.Fprintf(l.writer, "✗ %s\n", message)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Light) Stop() {
|
||||
// No cleanup needed for light indicator
|
||||
}
|
||||
|
||||
// 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 "line":
|
||||
return NewLineByLine()
|
||||
case "light":
|
||||
return NewLight()
|
||||
default:
|
||||
return NewLineByLine() // Default to line-by-line for better compatibility
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user