github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/progress.go (about)

     1  // Show the dynamic progress bar
     2  
     3  package cmd
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/fs/accounting"
    14  	"github.com/rclone/rclone/fs/log"
    15  	"github.com/rclone/rclone/fs/operations"
    16  	"github.com/rclone/rclone/lib/terminal"
    17  )
    18  
    19  const (
    20  	// interval between progress prints
    21  	defaultProgressInterval = 500 * time.Millisecond
    22  	// time format for logging
    23  	logTimeFormat = "2006/01/02 15:04:05"
    24  )
    25  
    26  // startProgress starts the progress bar printing
    27  //
    28  // It returns a func which should be called to stop the stats.
    29  func startProgress() func() {
    30  	stopStats := make(chan struct{})
    31  	oldLogPrint := fs.LogPrint
    32  	oldSyncPrint := operations.SyncPrintf
    33  
    34  	if !log.Redirected() {
    35  		// Intercept the log calls if not logging to file or syslog
    36  		fs.LogPrint = func(level fs.LogLevel, text string) {
    37  			printProgress(fmt.Sprintf("%s %-6s: %s", time.Now().Format(logTimeFormat), level, text))
    38  
    39  		}
    40  	}
    41  
    42  	// Intercept output from functions such as HashLister to stdout
    43  	operations.SyncPrintf = func(format string, a ...interface{}) {
    44  		printProgress(fmt.Sprintf(format, a...))
    45  	}
    46  
    47  	var wg sync.WaitGroup
    48  	wg.Add(1)
    49  	go func() {
    50  		defer wg.Done()
    51  		progressInterval := defaultProgressInterval
    52  		if ShowStats() && *statsInterval > 0 {
    53  			progressInterval = *statsInterval
    54  		}
    55  		ticker := time.NewTicker(progressInterval)
    56  		for {
    57  			select {
    58  			case <-ticker.C:
    59  				printProgress("")
    60  			case <-stopStats:
    61  				ticker.Stop()
    62  				printProgress("")
    63  				fs.LogPrint = oldLogPrint
    64  				operations.SyncPrintf = oldSyncPrint
    65  				fmt.Println("")
    66  				return
    67  			}
    68  		}
    69  	}()
    70  	return func() {
    71  		close(stopStats)
    72  		wg.Wait()
    73  	}
    74  }
    75  
    76  // state for the progress printing
    77  var (
    78  	nlines = 0 // number of lines in the previous stats block
    79  )
    80  
    81  // printProgress prints the progress with an optional log
    82  func printProgress(logMessage string) {
    83  	operations.StdoutMutex.Lock()
    84  	defer operations.StdoutMutex.Unlock()
    85  
    86  	var buf bytes.Buffer
    87  	w, _ := terminal.GetSize()
    88  	stats := strings.TrimSpace(accounting.GlobalStats().String())
    89  	logMessage = strings.TrimSpace(logMessage)
    90  
    91  	out := func(s string) {
    92  		buf.WriteString(s)
    93  	}
    94  
    95  	if logMessage != "" {
    96  		out("\n")
    97  		out(terminal.MoveUp)
    98  	}
    99  	// Move to the start of the block we wrote erasing all the previous lines
   100  	for i := 0; i < nlines-1; i++ {
   101  		out(terminal.EraseLine)
   102  		out(terminal.MoveUp)
   103  	}
   104  	out(terminal.EraseLine)
   105  	out(terminal.MoveToStartOfLine)
   106  	if logMessage != "" {
   107  		out(terminal.EraseLine)
   108  		out(logMessage + "\n")
   109  	}
   110  	fixedLines := strings.Split(stats, "\n")
   111  	nlines = len(fixedLines)
   112  	for i, line := range fixedLines {
   113  		if len(line) > w {
   114  			line = line[:w]
   115  		}
   116  		out(line)
   117  		if i != nlines-1 {
   118  			out("\n")
   119  		}
   120  	}
   121  	terminal.Write(buf.Bytes())
   122  }