func/progress/progress.go

207 lines
5.8 KiB
Go

package progress
import (
"fmt"
"io"
"os"
"time"
)
// Bar - a simple, unobtrusive progress indicator
// Usage:
// bar := New()
// bar.SetTotal(3)
// defer bar.Done()
// bar.Increment("Step 1")
// bar.Increment("Step 2")
// bar.Increment("Step 3")
// bar.Complete("Done")
//
// Instantiation creates a progress bar consisiting of an optional spinner
// prefix followed by an indicator of the current step, the total steps, and a
// trailing message The behavior differs substantially if verbosity is enbled
// or not. When in verbose mode, it is expected that the process is otherwise
// printing status information to output, in which case this bar only prints a
// single status update to output when the current step is updated,
// interspersing the status line with standard program output. When verbosity
// is not enabled (the default), it is expected that in general output is left
// clean. In this case the status is continually written to standard output
// along with an animated spinner. The status is written one line above current,
// such that any errors from the rest of the process begin at the beinning of a
// line. Finally, the bar respects headless mode, omitting any printing unless
// explicitly configured to do so, and even then not doing the continual write
// method but the single status update in the same manner as when verbosity is
// enabled.
// Format:
// [spinner] i/n t
type Bar struct {
out io.Writer
index int // Current step index
total int // Total steps
text string // Current display text
// verbose mode disables progress spinner and line overwrites, instead
// printing single, full line updates.
Verbose bool
// print verbose-style updates even when not attached to an interactive terminal.
printWhileHeadless bool
// Ticker for animated progress when non-verbose, interactive terminal.
ticker *time.Ticker
}
type Option func(*Bar)
func WithOutput(w io.Writer) Option {
return func(b *Bar) {
b.out = w
}
}
// WithVerbose indicates the system is in verbose mode, and writing an
// animated line via terminal codes would interrupt the flow of logs.
// When in verbose mode, the bar will print simple status update lines.
func WithVerbose(v bool) Option {
return func(b *Bar) {
b.Verbose = v
}
}
// WithPrintHeadless allows for overriding the default behavior of
// squelching all output when the terminal is detected as headless
// (noninteractive)
func WithPrintWhileHeadless(p bool) Option {
return func(b *Bar) {
b.printWhileHeadless = p
}
}
func New(options ...Option) *Bar {
b := &Bar{
out: os.Stdout,
}
for _, o := range options {
o(b)
}
return b
}
// SetTotal number of steps.
func (b *Bar) SetTotal(n int) {
b.total = n
}
// Increment the currenly active step, including beginning printing on first call.
func (b *Bar) Increment(text string) {
if b.index < b.total {
b.index++
}
b.text = text
// If this is not an interactive terminal, only print if explicitly set to
// print while headless, and even then, only a simple line write.
if !interactiveTerminal() {
if b.printWhileHeadless {
b.write()
}
return
}
// If we're in verbose mode, do a simple write
if b.Verbose {
b.write()
return
}
// Otherwise we are in non-verbose, interactive mode. Do a line-overwrite.
b.overwrite(" ") // Write with space for the spinner
// Start the spinner if not already started
if b.ticker == nil {
b.ticker = time.NewTicker(100 * time.Millisecond)
go b.spin(b.ticker.C)
}
}
// Complete the spinner by advancing to the last step, printing the final text and stopping the write loop.
func (b *Bar) Complete(text string) {
if !interactiveTerminal() && !b.printWhileHeadless {
return
}
b.index = b.total // Skip to last step
b.text = text
// If this is not an interactive terminal, only print if explicitly set to
// print while headless, and even then, only a simple line write.
if !interactiveTerminal() {
if b.printWhileHeadless {
b.write()
}
return
}
// If we're interactive, but in verbose mode do a simple write
if b.Verbose {
b.write()
return
}
// Otherwise we are in non-verbose mode with an interactive terminal.
// We should stop the spinner and write an unindented line.
b.Done() // stop spinner
}
// Done cancels the write loop if being used.
// Call in a defer statement after creation to ensure that the spinner stops
func (b *Bar) Done() {
if b.ticker != nil {
b.ticker.Stop()
b.ticker = nil
b.overwrite("") // write unindented
}
}
// Write a simple line status update.
func (b *Bar) write() {
fmt.Fprintf(b.out, "%v/%v %v\n", b.index, b.total, b.text)
}
// interactiveTerminal returns whether or not the currently attached process
// terminal is interactive. Used for determining whether or not to
// interactively prompt the user to confirm default choices, etc.
func interactiveTerminal() bool {
fi, err := os.Stdin.Stat()
return err == nil && ((fi.Mode() & os.ModeCharDevice) != 0)
}
const (
up = "\033[1A"
down = "\033[1B"
clear = "\033[K"
)
// overwrite the line prior with the bar text, optional prefix, creating an empty
// current line for potential errors/warnings.
func (b *Bar) overwrite(prefix string) {
// 1 Move to the front of the current line
// 2 Move up one line
// 3 Clear to the end of the line
// 4 Print status text with optional prefix (spinner)
// 5 Print linebreak such that subsequent messaes print correctly.
fmt.Fprintf(b.out, "\r%v%v%v%v/%v %v\n", up, clear, prefix, b.index, b.total, b.text)
}
// Write a spinner at the beginning of the previous line.
func (b *Bar) spin(ch <-chan time.Time) {
spinner := []string{"|", "/", "-", "\\"}
idx := 0
for range ch {
// Writes the spinner frame at the beginning of the previous line, moving
// the cursor back to the beginning of the current line for any errors or
// informative messages.
fmt.Fprintf(b.out, "\r%v%v%v\r", up, spinner[idx], down)
idx = (idx + 1) % 4
}
}