github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/progress.go (about)

     1  // Show the dynamic progress bar
     2  
     3  package cmd
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/ncw/rclone/fs"
    14  	"github.com/ncw/rclone/fs/accounting"
    15  	"github.com/ncw/rclone/fs/log"
    16  	"golang.org/x/crypto/ssh/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  	err := initTerminal()
    31  	if err != nil {
    32  		fs.Errorf(nil, "Failed to start progress: %v", err)
    33  		return func() {}
    34  	}
    35  	stopStats := make(chan struct{})
    36  	oldLogPrint := fs.LogPrint
    37  	if !log.Redirected() {
    38  		// Intercept the log calls if not logging to file or syslog
    39  		fs.LogPrint = func(level fs.LogLevel, text string) {
    40  			printProgress(fmt.Sprintf("%s %-6s: %s", time.Now().Format(logTimeFormat), level, text))
    41  
    42  		}
    43  	}
    44  	var wg sync.WaitGroup
    45  	wg.Add(1)
    46  	go func() {
    47  		defer wg.Done()
    48  		progressInterval := defaultProgressInterval
    49  		if ShowStats() && *statsInterval > 0 {
    50  			progressInterval = *statsInterval
    51  		}
    52  		ticker := time.NewTicker(progressInterval)
    53  		for {
    54  			select {
    55  			case <-ticker.C:
    56  				printProgress("")
    57  			case <-stopStats:
    58  				ticker.Stop()
    59  				printProgress("")
    60  				fs.LogPrint = oldLogPrint
    61  				fmt.Println("")
    62  				return
    63  			}
    64  		}
    65  	}()
    66  	return func() {
    67  		close(stopStats)
    68  		wg.Wait()
    69  	}
    70  }
    71  
    72  // VT100 codes
    73  const (
    74  	eraseLine         = "\x1b[2K"
    75  	moveToStartOfLine = "\x1b[0G"
    76  	moveUp            = "\x1b[A"
    77  )
    78  
    79  // state for the progress printing
    80  var (
    81  	nlines     = 0 // number of lines in the previous stats block
    82  	progressMu sync.Mutex
    83  )
    84  
    85  // printProgress prints the progress with an optional log
    86  func printProgress(logMessage string) {
    87  	progressMu.Lock()
    88  	defer progressMu.Unlock()
    89  
    90  	var buf bytes.Buffer
    91  	w, h, err := terminal.GetSize(int(os.Stdout.Fd()))
    92  	if err != nil {
    93  		w, h = 80, 25
    94  	}
    95  	_ = h
    96  	stats := strings.TrimSpace(accounting.Stats.String())
    97  	logMessage = strings.TrimSpace(logMessage)
    98  
    99  	out := func(s string) {
   100  		buf.WriteString(s)
   101  	}
   102  
   103  	if logMessage != "" {
   104  		out("\n")
   105  		out(moveUp)
   106  	}
   107  	// Move to the start of the block we wrote erasing all the previous lines
   108  	for i := 0; i < nlines-1; i++ {
   109  		out(eraseLine)
   110  		out(moveUp)
   111  	}
   112  	out(eraseLine)
   113  	out(moveToStartOfLine)
   114  	if logMessage != "" {
   115  		out(eraseLine)
   116  		out(logMessage + "\n")
   117  	}
   118  	fixedLines := strings.Split(stats, "\n")
   119  	nlines = len(fixedLines)
   120  	for i, line := range fixedLines {
   121  		if len(line) > w {
   122  			line = line[:w]
   123  		}
   124  		out(line)
   125  		if i != nlines-1 {
   126  			out("\n")
   127  		}
   128  	}
   129  	writeToTerminal(buf.Bytes())
   130  }