mirror of https://github.com/knative/func.git
207 lines
5.8 KiB
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
|
|
}
|
|
}
|