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 }